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つで構成されている。

  1. keyId
    signatureの認証に必要な「公開鍵」が記載されているtype:PersonのJSON(アカウント情報)のurl。リクエストする時にurlの「#main-key」は不要
  2. algorithm
    rsa-sha256。暗号化方式。RSAで暗号化してshaでハッシュ化(既存のモジュールをブラックボックスとして使えるので理解不要。呪文)
  3. headers
    署名の対象
  4. signature
    署名

【署名する対象】:draft版

署名の対象はheadersで指定する。

  1. (request-target)
  2. host
  3. date
  4. digest
  5. 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
hostexample.com
dateWed, 14 Dec 2022 07:28:00 GMT
digestSHA-256=Z2EHR3+Srb66MmKASfnRcbk83F8SBrW1LIiwx15/g80=
content-typeapplication/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で指定されているのが

  1. (request-target)
  2. host
  3. date
  4. digest
  5. content-type

だったらリクエストから該当するものをそのまま署名対象とする。
(request-target)だけは、REQUEST_METHODとREQUEST_URIの2つ必要。

飛んでくるリクエスト

REQUEST_METHODPOST
REQUEST_URI/inbox
HTTP_HOSTexample.com
HTTP_DATESun, 20 Oct 2024 21:30:16 GMT
HTTP_DIGESTSHA-256=ETw1NwooStIkE4vUyRfYU8udaBeNl4xR8237uCj2pEc=
CONTENT_TYPEapplication/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つで構成されている。

  1. (対象リスト)
    署名の対象
  2. alg
    rsa-v1_5-sha256。暗号化方式。RSAで暗号化してshaでハッシュ化(draft版の「rsa-sha256」と表記は違うけど同じもの、だと思う)
  3. keyid
    signatureの認証に必要な「公開鍵」が記載されているtype:PersonのJSON(アカウント情報)のurl。リクエストする時にurlの「#main-key」は不要
  4. created
    作成時間(秒)unixtime

署名の対象リストは「("@method" "@target-uri" "content-digest")」
必要なものが半角空白区切りでリストされている。

  1. @method
  2. @target-uri
  3. 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(ハッシュ)

署名の対象

@methodPOST
@target-urihttps://example.com/user/inbox
content-digestsha-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の対象リストで指定された要素を順番通りで使う。

  1. @method
  2. @target-uri
  3. content-digest

飛んでくるリクエスト

REQUEST_METHODPOST
REQUEST_SCHEMEhttps
HTTP_HOSTexample.com
REQUEST_URI/inbox
HTTP_DIGESTsha-256=:wkS96q13Uuy7sRhhA6g6sK0EgkzwQzBoWGCoawL6VeI=:
  • @method
    「REQUEST_METHOD」
  • @target-uri
    「REQUEST_SCHEME」://「HTTP_HOST」「REQUEST_URI」
  • content-digest
    「HTTP_DIGEST」

「@」で始まるものは「Derived Components(派生コンポーネント)」というらしく、HTTPの環境変数で取得できるものから組み立てる必要がある(環境変数の名前そのままでは使えない)
RFC9421のページに詳細があるので、公式の一次情報を確認してください。

https://datatracker.ietf.org/doc/html/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

Menu