Skip to content

Commit 76452a0

Browse files
authored
feat: ActivityPub HTTP Signature signing + account federation APIs (#9)
## Summary ActivityPub フェデレーションに必要な HTTP Signature 署名機能と、連携するアカウント関連 API を追加する。 ### 1. HTTP Signature Signing(Outbound) 他の ShuttlePub サービスが ActivityPub の投稿を配信する際に、Emumet がユーザーの秘密鍵で HTTP リクエストに署名する。 - **SigningKey エンティティ**(Repository パターン)+ Ed25519 暗号ドライバ - **デュアル署名**: Cavage draft-12(`Signature` ヘッダ)+ RFC 9421(`signature` / `signature-input` ヘッダ) - Account 作成時に SigningKey を自動生成 - **`POST /accounts/{account_id}/sign`** — HTTP リクエストへの署名 - **`GET /accounts/{account_id}/public-key`** — 公開鍵の取得(ActivityPub Actor publicKey 形式) ### 2. ActivityPub Account APIs(Inbound) ShuttlePub が ActivityPub ネットワーク内のローカルアカウントとして振る舞うためのエンドポイント。 - **WebFinger**: `GET /.well-known/webfinger` — ActivityPub のアカウント検出(`preferredUsername` → `accounts.name`) - **Actor**: `GET /accounts/{account_id}` — ActivityPub Actor ドキュメント(content negotiation 対応) - **Followers/Following**: `GET /accounts/{account_id}/followers|following` — OrderedCollection - **Inbox**: `POST /accounts/{account_id}/inbox` — HTTP Signature 検証付き受信 - Follow 受信時: 自動承認 + signed Accept をリモート inbox に配信 - Undo Follow 受信時: フォロー関係の削除 - **Outbox**: `GET /accounts/{account_id}/outbox` — 永続化された activity の配信 - **Account Name Validation**: `accounts.name` に UNIQUE 制約 + 空白禁止 ### 3. 受信 HTTP Signature 検証(Inbound Verification) Inbox で受信するリモートリクエストの署名検証。 - Cavage 署名検証(RSA / Ed25519) - Digest(SHA-256)+ Date ヘッダ検証 - SSRF 保護(プライベート IP / ローカルホスト拒否) - RFC 9421 は現時点で拒否(将来拡張用) ## Key Files | Layer | Files | |---|---| | **kernel** | `activitypub.rs` (DTO), `http_signing.rs` (inbound traits), `entity/signing_key.rs`, `entity/activitypub/outbox_activity.rs`, `repository/outbox_activity.rs` | | **driver** | `http_signing.rs` (Cavage verifier), `database/postgres/signing_key.rs`, `database/postgres/outbox_activity.rs` | | **application** | `service/activitypub.rs` (use cases), `service/signing_key.rs`, `transfer/activitypub.rs` | | **adapter** | `processor/account.rs` (find_by_name) | | **server** | `route/activitypub.rs`, `route/signing.rs`, `handler.rs` (DI), `main.rs`, `openapi.rs` | ## Migrations | Migration | Description | |---|---| | `20260329000001` | `signing_keys` テーブル作成 | | `20260621000002` | `accounts.name` UNIQUE 制約 | | `20260621000003` | `remote_accounts` に `inbox_url`, `public_key_pem` 追加 | | `20260621000004` | `outbox_activities` テーブル作成 | 全マイグレーションは冪等(`IF NOT EXISTS` / `DO \$\$ BEGIN ... EXCEPTION ... END \$\$;`)。 ## E2E Testing 4テストスイート・22テストが全てパスする統合E2Eテスト基盤を構築。 ### テスト構成 | Suite | テスト数 | 内容 | |---|---|---| | `e2e_basic_flow` | 4 | 基本 CRUD(アカウント作成・認証・プロフィール) | | `e2e_ap_mock` | 9 | Mock peer との AP 連携(WebFinger, Actor, Follow/Accept, 署名検証, Inbox 署名必須) | | `e2e_ap_iceshrimp` | 4 | 実 Iceshrimp v2026.5.1 とのクロスインスタンス S7-S9(Follow → Follow → 署名付き Create/Note) | | | `e2e_ap_mastodon` | 4 | 実 Mastodon v4.6.2 とのクロスインスタンス S7-S9(同上) | ### 自動ランナー - `e2e/run-ap-e2e.sh` — **単一の真実源**: cert生成 → compose起動 → サーバー起動 → 全テスト → cleanup - CI, `just e2e`, 手動実行の全てが同じスクリプトを呼ぶ - サービス readiness に `timeout` ベースのハード制限(120秒) - プロセス管理: `fuser -k` で確実なサーバー停止 - rootless Docker 対応: `DOCKER_HOST_IP` env var ### 修正されたバグ - **nginx 400 on Accept delivery**: `deliver_activity_to_inbox()` が重複 Host ヘッダーを送信していた問題を修正(cavage_headers から既存ヘッダーをフィルタリング) - **Iceshrimp Object ID validation**: `PUBLIC_BASE_URL` のポート `:8443` が原因で host mismatch — ポートなしに修正 - **Docker内部ルーティング**: Iceshrimp → Emumet の HTTPS接続に `listen 443 ssl;` を追加 - **Mastodon actor URL フォーマット**: Mastodon v4.x は `/ap/users/{numeric_id}` 形式を使用するが、テストキャッシュが `/users/{username}` でキーイングしていたため HTTP Signature 検証時にキャッシュミス — `verify["uri"]` 由来の正しい actor URL を使用するよう修正 - **rootless Docker `host.docker.internal`**: compose の `extra_hosts` に `DOCKER_HOST_IP` を設定し、nginx upstream がホストのルータブル IP 経由で Emumet に到達できるよう修正 - **Service readiness probe**: Iceshrimp/Mastodon の起動待機で `curl -sk` が nginx 502 を成功と誤判定 — HTTP ステータスコードまたはレスポンスボディで正しく判定するよう修正 - **Mastodon-sidekiq プローブ**: 存在しない heartbeat ファイルではなく `pgrep -f sidekiq` でプロセス存在確認 ### インフラ構成 - nginx リバースプロキシ: emumet / iceshrimp / mastodon の3ドメイン - 自己署名証明書: 4 SAN ドメイン(emumet / peer / iceshrimp / mastodon) - Iceshrimp v2026.5.1 + Mastodon v4.6.2 を含む Docker Compose 環境(`compose.yml` + `compose.ap-e2e.yml`) - Mastodon は nginx upstream に runtime variable + Docker DNS resolver を使用(起動順序問題を回避) ### CI - push (main, fix/signin) と PR (main) で `e2e/run-ap-e2e.sh` を実行(timeout 30分) - .env 自動生成(runner内蔵)— CI側の設定不要 ## Architecture ``` [Outbound Signing] ShuttlePub service → POST /accounts/{id}/sign → SigningKey → Cavage/RFC9421 sign → signed headers [Inbound Federation] Remote server → POST /accounts/{id}/inbox → HTTP Signature verify → Activity parse → Follow: auto-accept + signed Accept delivery → Undo Follow: remove follow relationship [Actor Discovery] Client → GET /accounts/{id} (Accept: application/activity+json) → content negotiation → ActivityPub Actor JSON-LD Client → GET /.well-known/webfinger?resource=acct:name@domain → WebFinger JRD response ```
2 parents 509cce7 + d8006d3 commit 76452a0

82 files changed

Lines changed: 13395 additions & 840 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,25 @@ REDIS_HOST=localhost
2020

2121
WORKER_ID=0
2222

23-
CORS_ALLOWED_ORIGINS=*
23+
CORS_ALLOWED_ORIGINS=*
24+
25+
# Required when running with test-mode feature:
26+
# EMUMET_TEST_MODE_TOKEN=your-test-token
27+
28+
PUBLIC_BASE_URL=http://localhost:8080
29+
#
30+
# For AP E2E tests (with compose.ap-e2e.yml nginx proxy):
31+
# PUBLIC_BASE_URL=https://emumet.127.0.0.1.nip.io
32+
# PEER_BASE_URL=https://peer.127.0.0.1.nip.io:8443
33+
#
34+
# For AP E2E compose environment:
35+
# Set these when running with compose.ap-e2e.yml
36+
# The server API goes through direct HTTP to avoid nginx proxy issues,
37+
# while the public URL is used for ActivityPub ID generation and signing.
38+
# EMUMET_E2E_SERVER_BASE_URL=http://localhost:8080
39+
# EMUMET_E2E_PUBLIC_BASE_URL=https://emumet.127.0.0.1.nip.io
40+
# PUBLIC_BASE_URL=${EMUMET_E2E_PUBLIC_BASE_URL}
41+
# EMUMET_E2E_EXTERNAL_SERVER=1
42+
# AP_TEST_ALLOWED_FETCH_HOSTS=127.0.0.1,iceshrimp.127.0.0.1.nip.io # Allowlisted hosts for test-mode AP fetches
43+
# AP_TEST_ACCEPT_INVALID_CERTS=1 # Accept self-signed certs in test-mode (value must be "1", not "true")
44+
# ICESHRIMP_BASE_URL=https://iceshrimp.127.0.0.1.nip.io:8443

.github/workflows/e2e.yml

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,42 +21,10 @@ jobs:
2121
- name: Cache Rust build artifacts
2222
uses: Swatinem/rust-cache@v2
2323

24-
- name: Install just
25-
uses: extractions/setup-just@v2
26-
27-
- name: Install cargo-binstall
28-
uses: cargo-bins/cargo-binstall@main
29-
30-
- name: Install sqlx-cli
31-
run: cargo binstall sqlx-cli --no-confirm
32-
3324
- name: Create master-key-password file
3425
run: |
3526
echo -n "ci-test-master-key-password" > master-key-password
3627
chmod 600 master-key-password
3728
38-
- name: Create .env file
39-
run: |
40-
{
41-
echo "DATABASE_URL=postgres://postgres:develop@localhost:5432/postgres"
42-
echo "HYDRA_ISSUER_URL=http://localhost:4444/"
43-
echo "HYDRA_ADMIN_URL=http://localhost:4445/"
44-
echo "KRATOS_PUBLIC_URL=http://localhost:4433/"
45-
echo "EXPECTED_AUDIENCE=account"
46-
echo "KETO_READ_URL=http://localhost:4466"
47-
echo "KETO_WRITE_URL=http://localhost:4467"
48-
echo "REDIS_URL=redis://localhost:6379"
49-
echo "WORKER_ID=0"
50-
echo "CORS_ALLOWED_ORIGINS=*"
51-
} > .env
52-
5329
- name: Run E2E tests
54-
run: just e2e
55-
56-
- name: Collect docker logs on failure
57-
if: failure()
58-
run: docker compose logs
59-
60-
- name: Stop services
61-
if: always()
62-
run: just down
30+
run: bash e2e/run-ap-e2e.sh

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
.env
66
logs
77
master-key-password
8+
e2e/certs/*.pem
9+
e2e/certs/*.key

0 commit comments

Comments
 (0)