Files
activity-pub/README.md
gilesb 5ebfdac887 Add scoped tokens, L2-side revocation, and security docs
Security improvements:
- Tokens now include optional l1_server claim for scoping
- /auth/verify checks token scope matches requesting L1
- L2 maintains revoked_tokens table - even if L1 ignores revoke, token fails
- Logout revokes token in L2 db before notifying L1s
- /renderers/attach creates scoped tokens (not embedded in HTML)
- Add get_token_claims() to auth.py

Database:
- Add revoked_tokens table with token_hash, username, expires_at
- Add db functions: revoke_token, is_token_revoked, cleanup_expired_revocations

Documentation:
- Document security features in README

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:21:13 +00:00

250 lines
7.4 KiB
Markdown

# Art DAG L2 Server - ActivityPub
Ownership registry and ActivityPub federation for Art DAG.
## What it does
- **Registry**: Maintains owned assets with content hashes
- **Activities**: Creates signed ownership claims (Create activities)
- **Federation**: ActivityPub endpoints for follow/share
- **L1 Integration**: Records completed L1 runs as owned assets
- **Authentication**: User registration, login, JWT tokens
## Setup
```bash
pip install -r requirements.txt
# Configure (optional - defaults shown)
export ARTDAG_DOMAIN=artdag.rose-ash.com
export ARTDAG_USER=giles
export ARTDAG_DATA=~/.artdag/l2
export DATABASE_URL=postgresql://artdag:artdag@localhost:5432/artdag
export L1_SERVERS=https://celery-artdag.rose-ash.com
# Generate signing keys (required for federation)
python setup_keys.py
# Start server
python server.py
```
## JWT Secret Configuration
The JWT secret is used to sign authentication tokens. **Without a persistent secret, tokens are invalidated on server restart.**
### Generate a secret
```bash
# Generate a 64-character hex secret
openssl rand -hex 32
# Or with Python
python -c "import secrets; print(secrets.token_hex(32))"
```
### Local development
```bash
export JWT_SECRET="your-generated-secret-here"
python server.py
```
### Docker Swarm (recommended for production)
Create a Docker secret:
```bash
# From a generated value
openssl rand -hex 32 | docker secret create jwt_secret -
# Or from a file
echo "your-secret-here" > jwt_secret.txt
docker secret create jwt_secret jwt_secret.txt
rm jwt_secret.txt
```
Reference in docker-compose.yml:
```yaml
services:
l2-server:
secrets:
- jwt_secret
secrets:
jwt_secret:
external: true
```
The server reads secrets from `/run/secrets/jwt_secret` automatically.
## Key Setup
ActivityPub requires RSA keys for signing activities. Generate them:
```bash
# Local
python setup_keys.py
# Or with custom paths
python setup_keys.py --data-dir /data/l2 --user giles
# In Docker, exec into container or mount volume
docker exec -it <container> python setup_keys.py
```
Keys are stored in `$ARTDAG_DATA/keys/`:
- `{username}.pem` - Private key (chmod 600, NEVER share)
- `{username}.pub` - Public key (included in actor profile)
**Important**: Private keys are gitignored. Back them up securely. Losing them invalidates all your signatures.
## L1 Renderers Configuration
The `L1_SERVERS` environment variable defines which L1 rendering servers are available to users. Users can attach to these servers from the Renderers page to run effects and manage media.
```bash
# Single server (default)
export L1_SERVERS=https://celery-artdag.rose-ash.com
# Multiple servers (comma-separated)
export L1_SERVERS=https://celery-artdag.rose-ash.com,https://renderer2.example.com,https://renderer3.example.com
```
When a user attaches to an L1 server:
1. L2 creates a **scoped token** that only works for that specific L1
2. User is redirected to the L1's `/auth` endpoint with the scoped token
3. L1 calls back to L2's `/auth/verify` endpoint to validate the token
4. L2 verifies the token scope matches the requesting L1
5. L1 sets its own local cookie, logging the user in
6. The attachment is recorded in L2's `user_renderers` table
### Security Features
**No shared secrets**: L1 servers verify tokens by calling L2's `/auth/verify` endpoint. Any L1 provider can federate without the JWT secret.
**L1 authorization**: Only servers listed in `L1_SERVERS` can verify tokens. L1 must identify itself in verify requests.
**Scoped tokens**: Tokens issued for attachment contain an `l1_server` claim. A token scoped to L1-A cannot be used on L1-B, preventing malicious L1s from stealing tokens for use elsewhere.
**Federated logout**: When a user logs out of L2:
1. L2 revokes the token in its database (`revoked_tokens` table)
2. L2 calls `/auth/revoke` on all attached L1s to revoke their local copies
3. All attachments are removed from `user_renderers`
Even if a malicious L1 ignores the revoke call, the token will fail verification at L2 because it's in the revocation table.
**Token revocation on L1**: L1 servers maintain their own Redis-based revocation list and check it on every authenticated request.
Users can manage attachments at `/renderers`.
## Web UI
The server provides a web interface:
| Path | Description |
|------|-------------|
| `/` | Home page with stats and README |
| `/assets` | Browse registered assets |
| `/asset/{name}` | Asset detail page |
| `/activities` | View published activities |
| `/activity/{id}` | Activity detail page |
| `/users` | List registered users |
| `/renderers` | Manage L1 renderer connections |
| `/anchors/ui` | OpenTimestamps anchor management |
| `/login` | Login page |
| `/register` | Registration page |
| `/logout` | Log out |
| `/download/client` | Download CLI client |
## Client Commands
### Upload Media
Register a media asset (image, video, audio) with a content hash:
```bash
curl -X POST http://localhost:8200/assets \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"name": "my-video",
"content_hash": "abc123...",
"asset_type": "video",
"tags": ["art", "generated"]
}'
```
### Upload Recipe
Record an L1 run as an owned asset. This fetches the run details from the L1 server and registers the output:
```bash
curl -X POST http://localhost:8200/assets/record-run \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"run_id": "uuid-from-l1",
"l1_server": "https://celery-artdag.rose-ash.com",
"output_name": "my-rendered-video"
}'
```
## API Endpoints
### Server Info
| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | Home page with stats |
### Assets
| Method | Path | Description |
|--------|------|-------------|
| GET | `/assets` | List all assets |
| GET | `/assets/{name}` | Get asset by name |
| POST | `/assets` | Upload media - register new asset |
| POST | `/assets/record-run` | Upload recipe - record L1 run |
### ActivityPub
| Method | Path | Description |
|--------|------|-------------|
| GET | `/.well-known/webfinger?resource=acct:user@domain` | Actor discovery |
| GET | `/users/{username}` | Actor profile |
| GET | `/users/{username}/outbox` | Published activities |
| POST | `/users/{username}/inbox` | Receive activities |
| GET | `/users/{username}/followers` | Followers list |
| GET | `/objects/{content_hash}` | Get object by hash |
| GET | `/activities/{index}` | Get activity by index |
### Authentication
| Method | Path | Description |
|--------|------|-------------|
| POST | `/auth/register` | Register new user (API) |
| POST | `/auth/login` | Login, get JWT token (API) |
| GET | `/auth/me` | Get current user |
| POST | `/auth/verify` | Verify token (for L1 servers) |
## Data Storage
Data stored in PostgreSQL:
- `users` - Registered users
- `assets` - Asset registry
- `activities` - Signed activities
- `followers` - Followers list
RSA keys stored in `$ARTDAG_DATA/keys/` (files, not database).
## Architecture
```
L2 Server (port 8200)
├── POST /assets (upload media) → Register asset → Create activity → Sign
├── POST /assets/record-run (upload recipe) → Fetch L1 run → Register output
│ │
│ └── GET L1_SERVER/runs/{id}
├── GET /users/{user}/outbox → Return signed activities
└── POST /users/{user}/inbox → Receive Follow requests
```