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>
250 lines
7.4 KiB
Markdown
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
|
|
```
|