Commit 76452a0
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
```82 files changed
Lines changed: 13395 additions & 840 deletions
File tree
- .github/workflows
- adapter/src/processor
- application
- src
- service
- transfer
- driver
- src
- crypto
- database
- postgres
- e2e
- certs
- iceshrimp/.config
- mastodon/.config
- kernel
- src
- entity
- account
- activitypub
- repository
- test_utils
- migrations
- server
- src
- route
- schema
- tests
- support
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | 24 | | |
34 | 25 | | |
35 | 26 | | |
36 | 27 | | |
37 | 28 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | 29 | | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
0 commit comments