# FlaskPaste API Reference ## Overview FlaskPaste provides a RESTful API for creating, retrieving, and deleting text and binary pastes. **Base URL:** `http://your-server:5000/` **Content Types:** - Requests: `application/json`, `text/plain`, `application/octet-stream`, or any binary type - Responses: `application/json` for metadata, original MIME type for raw content ## Authentication Authentication is optional and uses client certificate fingerprints passed via the `X-SSL-Client-SHA1` header. ```http X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 ``` The fingerprint must be exactly 40 lowercase hexadecimal characters (SHA1). **Benefits of authentication:** - Larger upload limit (50 MiB vs 3 MiB) - Longer default expiry (7-30 days vs 1 day) - Ability to list and delete owned pastes - Register via PKI for trusted status and admin eligibility --- ## Endpoints ### GET /challenge Get a proof-of-work challenge for paste creation. Required when PoW is enabled. **Request:** ```http GET /challenge HTTP/1.1 Host: localhost:5000 ``` **Response (PoW disabled):** ```json { "enabled": false, "difficulty": 0 } ``` **Response (PoW enabled):** ```json { "enabled": true, "nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8", "difficulty": 20, "base_difficulty": 20, "elevated": false, "expires": 1700000300, "token": "a1b2c3d4...:1700000300:20:signature" } ``` **Response (PoW enabled, anti-flood active):** ```json { "enabled": true, "nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8", "difficulty": 24, "base_difficulty": 20, "elevated": true, "expires": 1700000300, "token": "a1b2c3d4...:1700000300:24:signature" } ``` The client must find a number `N` such that `SHA256(nonce + ":" + N)` has at least `difficulty` leading zero bits. When `elevated` is true, the difficulty has been increased due to high request volume (anti-flood protection). The `base_difficulty` shows the normal difficulty level. --- ### GET /health Health check endpoint for load balancers and monitoring. **Request:** ```http GET /health HTTP/1.1 Host: localhost:5000 ``` **Response (200 OK):** ```json { "status": "healthy", "database": "ok" } ``` **Response (503 Service Unavailable):** ```json { "status": "unhealthy", "database": "error" } ``` --- ### GET / Returns API information and usage instructions. **Request:** ```http GET / HTTP/1.1 Host: localhost:5000 ``` **Response:** ```json { "name": "FlaskPaste", "version": "1.5.0", "endpoints": { "GET /": "API information", "GET /health": "Health check", "POST /": "Create paste", "GET /": "Retrieve paste metadata", "GET //raw": "Retrieve raw paste content", "DELETE /": "Delete paste" }, "usage": { "raw": "curl --data-binary @file.txt http://host/", "pipe": "cat file.txt | curl --data-binary @- http://host/", "json": "curl -H 'Content-Type: application/json' -d '{\"content\":\"...\"}' http://host/" }, "note": "Use --data-binary (not -d) to preserve newlines" } ``` --- ### GET /pastes List pastes for the authenticated user. Requires authentication. **Request:** ```http GET /pastes HTTP/1.1 Host: localhost:5000 X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Maximum results (default: 100, max: 1000) | | `offset` | int | Skip first N results (default: 0) | | `type` | string | Filter by MIME type (glob pattern, e.g., `image/*`) | | `after` | int | Created after timestamp (Unix epoch) | | `before` | int | Created before timestamp (Unix epoch) | | `all` | int | List all pastes (admin only, set to 1) | **Response (200 OK):** ```json { "pastes": [ { "id": "a1b2c3d4e5f6", "size": 1234, "mime_type": "text/plain", "created_at": "2024-12-20T10:30:00Z", "expires_at": "2024-12-25T10:30:00Z", "burn_after_read": false, "password_protected": false, "owner": "a1b2c3d4..." } ], "total": 42, "limit": 100, "offset": 0 } ``` **Response (401 Unauthorized):** ```json { "error": "Authentication required" } ``` **Response (403 Forbidden - non-admin using `all=1`):** ```json { "error": "Admin access required" } ``` **Notes:** - Only admin users can use `all=1` to list all pastes - The `owner` field shows the certificate fingerprint (truncated) - First user to register via PKI becomes admin --- ### POST / Create a new paste. **Request (Raw Binary):** ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: application/octet-stream ``` **Request (JSON):** ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: application/json {"content": "Hello, World!"} ``` **Request (with Proof-of-Work):** When PoW is enabled, include the challenge token and solution: ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: text/plain X-PoW-Token: a1b2c3d4...:1700000300:20:signature X-PoW-Solution: 12345678 Hello, World! ``` **Request (Burn-After-Read):** Create a paste that deletes itself after first retrieval: ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: text/plain X-Burn-After-Read: true Secret message ``` **Request (Custom Expiry):** Create a paste with custom expiry time (in seconds): ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: text/plain X-Expiry: 3600 Expires in 1 hour ``` **Request (Password Protected):** Create a paste that requires a password to access: ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: text/plain X-Paste-Password: secretpassword Password protected content ``` **Response (201 Created):** ```json { "id": "abc12345", "url": "/abc12345", "raw": "/abc12345/raw", "mime_type": "text/plain", "created_at": 1700000000, "owner": "a1b2c3...", // Only present if authenticated "burn_after_read": true, // Only present if enabled "expires_at": 1700003600, // Only present if custom expiry set "password_protected": true // Only present if password set } ``` **Errors:** | Code | Description | |------|-------------| | 400 | No content provided | | 400 | Password too long (max 1024 chars) | | 400 | Proof-of-work required (when PoW enabled) | | 400 | Proof-of-work failed (invalid/expired challenge) | | 400 | Paste too small (below minimum size) | | 413 | Paste too large | | 429 | Duplicate content rate limit exceeded | | 429 | Rate limit exceeded (per-IP throttling) | **Size Limits:** - Minimum: disabled by default (`FLASKPASTE_MIN_SIZE`, e.g. 64 bytes for encryption enforcement) - Anonymous: 3 MiB (configurable via `FLASKPASTE_MAX_ANON`) - Authenticated: 50 MiB (configurable via `FLASKPASTE_MAX_AUTH`) **Rate Limit Headers:** When rate limiting is enabled, responses include standard rate limit headers: | Header | Description | |--------|-------------| | `X-RateLimit-Limit` | Maximum requests per window | | `X-RateLimit-Remaining` | Remaining requests in current window | | `X-RateLimit-Reset` | Unix timestamp when window resets | These headers appear on both successful (201) and rate-limited (429) responses: ```http HTTP/1.1 201 Created X-RateLimit-Limit: 10 X-RateLimit-Remaining: 9 X-RateLimit-Reset: 1700000060 ``` ```http HTTP/1.1 429 Too Many Requests Retry-After: 45 X-RateLimit-Limit: 10 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1700000060 ``` Rate limits are per-IP and configurable: - `FLASKPASTE_RATE_MAX`: Base limit (default: 10 requests/minute) - `FLASKPASTE_RATE_AUTH_MULT`: Multiplier for authenticated users (default: 5x) --- ### GET /{id} ### HEAD /{id} Retrieve paste metadata. HEAD returns headers only (no body). **Request:** ```http GET /abc12345 HTTP/1.1 Host: localhost:5000 ``` **Request (Password Protected):** ```http GET /abc12345 HTTP/1.1 Host: localhost:5000 X-Paste-Password: secretpassword ``` **Response (200 OK):** ```json { "id": "abc12345", "mime_type": "text/plain", "size": 1234, "created_at": 1700000000, "raw": "/abc12345/raw", "password_protected": true // Only present if protected } ``` **Errors:** | Code | Description | |------|-------------| | 400 | Invalid paste ID format | | 401 | Password required | | 403 | Invalid password | | 404 | Paste not found | --- ### GET /{id}/raw ### HEAD /{id}/raw Retrieve raw paste content with correct MIME type. HEAD returns headers only (useful for checking MIME type and size without downloading content). **Request:** ```http GET /abc12345/raw HTTP/1.1 Host: localhost:5000 ``` **Request (Password Protected):** ```http GET /abc12345/raw HTTP/1.1 Host: localhost:5000 X-Paste-Password: secretpassword ``` **Response (200 OK):** ```http HTTP/1.1 200 OK Content-Type: image/png Content-Disposition: inline ``` - `Content-Disposition: inline` is set for `image/*` and `text/*` types - Content-Type matches the detected/stored MIME type **Errors:** | Code | Description | |------|-------------| | 400 | Invalid paste ID format | | 401 | Password required | | 403 | Invalid password | | 404 | Paste not found | --- ### DELETE /{id} Delete a paste. Requires authentication and ownership (or admin rights). **Request:** ```http DELETE /abc12345 HTTP/1.1 Host: localhost:5000 X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 ``` **Response (200 OK):** ```json { "message": "Paste deleted" } ``` **Errors:** | Code | Description | |------|-------------| | 400 | Invalid paste ID format | | 401 | Authentication required | | 403 | Permission denied (not owner or admin) | | 404 | Paste not found | **Notes:** - Admin users can delete any paste, not just their own - First user to register via PKI becomes admin --- ## MIME Type Detection FlaskPaste automatically detects MIME types using: 1. **Magic byte signatures** (highest priority) | Category | Formats | |----------|---------| | Images | PNG, JPEG, GIF, WebP, BMP, TIFF, ICO | | Video | MP4, WebM, FLV, Matroska | | Audio | MP3, FLAC, OGG | | Documents | PDF, MS Office (DOC/XLS/PPT), ZIP-based (DOCX/XLSX/ODT) | | Executables | EXE/DLL (PE), ELF (Linux), Mach-O (macOS), WASM | | Archives | ZIP, GZIP, BZIP2, XZ, ZSTD, LZ4, 7z, RAR | | Data | SQLite | 2. **Explicit Content-Type header** (if not generic) 3. **UTF-8 detection** (falls back to `text/plain`) 4. **Binary fallback** (`application/octet-stream`) --- ## Paste Expiry Pastes automatically expire based on authentication level: | Trust Level | Default Expiry | Description | |-------------|----------------|-------------| | Anonymous | 1 day | No certificate provided | | Untrusted | 7 days | Certificate not registered via PKI | | Trusted | 30 days | Certificate registered via `/register` | - Every `GET /{id}` or `GET /{id}/raw` updates the last access timestamp - Cleanup runs automatically (hourly, throttled) **Custom Expiry:** Pastes can have custom expiry times using the `X-Expiry` header: ```bash # Paste expires in 1 hour curl -H "X-Expiry: 3600" --data-binary @file.txt http://host/ ``` **Configuration:** ```bash export FLASKPASTE_EXPIRY_ANON=86400 # Anonymous: 1 day export FLASKPASTE_EXPIRY_UNTRUSTED=604800 # Untrusted cert: 7 days export FLASKPASTE_EXPIRY_TRUSTED=2592000 # Trusted cert: 30 days export FLASKPASTE_MAX_EXPIRY=7776000 # Max custom expiry: 90 days ``` **Notes:** - Custom expiry is capped at `FLASKPASTE_MAX_EXPIRY` - Invalid or negative values use the default for the user's trust level - All pastes now include `expires_at` timestamp in responses --- ## Burn-After-Read Single-access pastes that delete themselves after first retrieval. **How it works:** - Set `X-Burn-After-Read: true` header on creation - First `GET /{id}/raw` returns content and deletes paste - Subsequent requests return 404 - Metadata `GET /{id}` does not trigger burn - `HEAD` requests do not trigger burn **Usage:** ```bash # Create burn-after-read paste curl -H "X-Burn-After-Read: true" --data-binary @secret.txt http://host/ # Response indicates burn is enabled { "id": "abc12345", "burn_after_read": true, ... } ``` **Notes:** - Response includes `X-Burn-After-Read: true` header when content is retrieved - Can be combined with custom expiry (paste expires OR burns, whichever first) - Accepted values: `true`, `1`, `yes` (case-insensitive) --- ## Password Protection Pastes can be protected with a password using PBKDF2-HMAC-SHA256 hashing. **Creating a protected paste:** ```http POST / HTTP/1.1 Host: localhost:5000 Content-Type: text/plain X-Paste-Password: mysecretpassword Protected content here ``` **Response (201 Created):** ```json { "id": "abc12345", "url": "/abc12345", "raw": "/abc12345/raw", "mime_type": "text/plain", "created_at": 1700000000, "password_protected": true } ``` **Accessing protected paste:** ```http GET /abc12345 HTTP/1.1 Host: localhost:5000 X-Paste-Password: mysecretpassword ``` **Errors:** | Code | Description | |------|-------------| | 400 | Password too long (max 1024 chars) | | 401 | Password required | | 403 | Invalid password | **Response (401 Unauthorized):** ```json { "error": "Password required", "password_protected": true } ``` **Response (403 Forbidden):** ```json { "error": "Invalid password" } ``` **Security Implementation:** - PBKDF2-HMAC-SHA256 with 600,000 iterations (OWASP 2023) - 32-byte random salt per password - Constant-time comparison prevents timing attacks - Passwords never logged or stored in plaintext - Maximum 1024 characters to prevent DoS via expensive hashing **CLI Usage:** ```bash # Create protected paste ./fpaste create -p "mypassword" secret.txt # Retrieve protected paste ./fpaste get -p "mypassword" abc12345 ``` **Notes:** - Password protection can be combined with burn-after-read and custom expiry - Unicode passwords are supported - Special characters are allowed --- ## Abuse Prevention FlaskPaste includes content-hash based deduplication to prevent spam and abuse. **How it works:** - Each paste's SHA256 content hash is tracked - Repeated submissions of identical content are throttled - After exceeding the threshold, further duplicates are rejected with 429 **Default limits:** - Window: 1 hour (`FLASKPASTE_DEDUP_WINDOW`) - Maximum: 3 identical submissions per window (`FLASKPASTE_DEDUP_MAX`) **Response (429 Too Many Requests):** ```json { "error": "Duplicate content rate limit exceeded", "count": 3, "window_seconds": 3600 } ``` **Configuration:** ```bash export FLASKPASTE_DEDUP_WINDOW=3600 # Window in seconds (default: 1 hour) export FLASKPASTE_DEDUP_MAX=3 # Max duplicates per window (default: 3) ``` **Notes:** - Different content is not affected by other content's limits - Counter resets after the window expires - Hash records are cleaned up periodically --- ## Entropy Enforcement FlaskPaste can require minimum content entropy to enforce client-side encryption. **How it works:** - Shannon entropy is calculated for submitted content (bits per byte) - Encrypted/random data: ~7.5-8.0 bits/byte - English text: ~4.0-5.0 bits/byte - Content below threshold is rejected with 400 **Configuration:** ```bash export FLASKPASTE_MIN_ENTROPY=6.0 # Require encryption-level entropy (0=disabled) export FLASKPASTE_MIN_ENTROPY_SIZE=256 # Only check content >= this size (default: 256) ``` **Response (400 Bad Request):** ```json { "error": "Content entropy too low", "entropy": 4.12, "min_entropy": 7.0, "hint": "Encrypt content before uploading (fpaste encrypts by default)" } ``` **Caveats:** - Small data is exempt (configurable via `MIN_ENTROPY_SIZE`, default 256 bytes) - Compressed data (gzip, zip) also has high entropy — not distinguishable from encrypted - This is a heuristic, not cryptographic proof of encryption **Recommended thresholds:** | Threshold | Effect | |-----------|--------| | 0 | Disabled (default) | | 5.0 | Blocks most plaintext | | 6.0 | Requires encryption or compression | | 7.0 | Requires encryption + sufficient size | --- ### Binary Content Requirement FlaskPaste can require unrecognizable binary content (encryption enforcement). **How it works:** - Content is checked for valid UTF-8 text - Plaintext (valid UTF-8) is rejected with 400 - Only binary content (invalid UTF-8) is allowed **Configuration:** ```bash export FLASKPASTE_REQUIRE_BINARY=1 # Reject plaintext (0=disabled) ``` **Response (400 Bad Request):** ```json { "error": "Recognizable format not allowed", "detected": "text/plain", "hint": "Encrypt content before uploading (fpaste encrypts by default)" } ``` **vs Entropy enforcement:** | Method | Detects | Use case | |--------|---------|----------| | Entropy | Low-entropy data | Reject unencrypted content | | Binary | Valid UTF-8 text | Reject plaintext | Use both together for maximum encryption enforcement: ```bash export FLASKPASTE_REQUIRE_BINARY=1 export FLASKPASTE_MIN_ENTROPY=6.0 export FLASKPASTE_MIN_SIZE=64 ``` --- ## Proof-of-Work FlaskPaste includes an optional proof-of-work system to prevent automated spam. **How it works:** 1. Client requests a challenge via `GET /challenge` 2. Server returns a nonce, difficulty, expiry time, and signed token 3. Client computes SHA256 hashes until finding one with enough leading zero bits 4. Client submits paste with `X-PoW-Token` and `X-PoW-Solution` headers **Algorithm:** ``` For N = 0, 1, 2, ...: hash = SHA256(nonce + ":" + N) if leading_zero_bits(hash) >= difficulty: return N as solution ``` **Configuration:** ```bash export FLASKPASTE_POW_DIFFICULTY=20 # Leading zero bits required (0=disabled) export FLASKPASTE_POW_TTL=300 # Challenge validity in seconds export FLASKPASTE_POW_SECRET="..." # Optional signing key (auto-generated if empty) ``` **Headers:** | Header | Description | |--------|-------------| | `X-PoW-Token` | The full token from `/challenge` response | | `X-PoW-Solution` | The computed solution number | **Error responses:** ```json { "error": "Proof-of-work required", "hint": "GET /challenge for a new challenge" } ``` ```json { "error": "Proof-of-work failed: Challenge expired" } ``` **Notes:** - Difficulty 20 requires approximately 1 million hash attempts on average - Challenges are signed to prevent tampering - Each challenge can only be used once (nonce uniqueness) --- ## Error Response Format All errors return JSON: ```json { "error": "Description of the error" } ``` For size limit errors (413): ```json { "error": "Paste too large", "size": 5000000, "max_size": 3145728, "authenticated": false } ``` --- ## Security Headers All responses include the following security headers: | Header | Value | |--------|-------| | `X-Content-Type-Options` | `nosniff` | | `X-Frame-Options` | `DENY` | | `Referrer-Policy` | `strict-origin-when-cross-origin` | | `Content-Security-Policy` | `default-src 'none'; frame-ancestors 'none'` | | `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | | `Cache-Control` | `no-store, no-cache, must-revalidate, private` | | `Pragma` | `no-cache` | --- ## Request Tracing All requests include an `X-Request-ID` header for log correlation: - If the client provides `X-Request-ID`, it is passed through - If not provided, a UUID is generated - The ID is echoed back in the response - All log entries include `[rid=]` **Example:** ```bash # Client-provided ID curl -H "X-Request-ID: my-trace-123" https://paste.example.com/health # Response includes: # X-Request-ID: my-trace-123 ``` --- ## Proxy Trust Validation When `FLASKPASTE_PROXY_SECRET` is configured, the application validates that requests come from a trusted reverse proxy by checking the `X-Proxy-Secret` header. This provides defense-in-depth against header spoofing if an attacker bypasses the reverse proxy. **Configuration:** ```bash export FLASKPASTE_PROXY_SECRET="your-secret-value" ``` **Proxy Configuration (HAProxy):** ``` http-request set-header X-Proxy-Secret your-secret-value ``` If the secret doesn't match, authentication headers (`X-SSL-Client-SHA1`) are ignored and the request is treated as anonymous. --- ## PKI (Certificate Authority) FlaskPaste includes an optional minimal PKI for issuing client certificates. ### Configuration Enable PKI via environment variables: ```bash export FLASKPASTE_PKI_ENABLED=1 # Enable PKI endpoints export FLASKPASTE_PKI_CA_PASSWORD="secret" # Required: CA password export FLASKPASTE_PKI_CERT_DAYS=365 # Client certificate validity (days) export FLASKPASTE_PKI_CA_DAYS=3650 # CA certificate validity (days) ``` ### Certificate Revocation When PKI is enabled, certificates issued by the CA are tracked. Revoked certificates are rejected during authentication (treated as anonymous). --- ### GET /pki Get PKI status and CA information. **Request:** ```http GET /pki HTTP/1.1 Host: localhost:5000 ``` **Response (PKI disabled):** ```json { "enabled": false } ``` **Response (PKI enabled, no CA):** ```json { "enabled": true, "ca_exists": false, "hint": "POST /pki/ca to generate CA" } ``` **Response (PKI enabled with CA):** ```json { "enabled": true, "ca_exists": true, "common_name": "FlaskPaste CA", "fingerprint_sha1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "created_at": 1700000000, "expires_at": 2015000000, "key_algorithm": "ec:secp384r1" } ``` --- ### POST /pki/ca Generate a new Certificate Authority. Only works once (first-run bootstrap). **Request:** ```http POST /pki/ca HTTP/1.1 Host: localhost:5000 Content-Type: application/json {"common_name": "My CA"} ``` **Response (201 Created):** ```json { "message": "CA generated", "common_name": "My CA", "fingerprint_sha1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "created_at": 1700000000, "expires_at": 2015000000, "download": "/pki/ca.crt" } ``` **Errors:** | Code | Description | |------|-------------| | 404 | PKI not enabled | | 409 | CA already exists | | 500 | PKI_CA_PASSWORD not configured | --- ### GET /pki/ca.crt Download CA certificate in PEM format (for trust store). **Request:** ```http GET /pki/ca.crt HTTP/1.1 Host: localhost:5000 ``` **Response (200 OK):** ``` -----BEGIN CERTIFICATE----- MIICxDCCAaygAwIBAgIUY... -----END CERTIFICATE----- ``` Content-Type: `application/x-pem-file` **Errors:** | Code | Description | |------|-------------| | 404 | PKI not enabled or CA not initialized | --- ### POST /pki/issue Issue a new client certificate (open registration). **Request:** ```http POST /pki/issue HTTP/1.1 Host: localhost:5000 Content-Type: application/json {"common_name": "alice"} ``` **Response (201 Created):** ```json { "message": "Certificate issued", "serial": "00000000000000000000000000000001", "common_name": "alice", "fingerprint_sha1": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", "created_at": 1700000000, "expires_at": 1731536000, "certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", "private_key_pem": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" } ``` **Errors:** | Code | Description | |------|-------------| | 400 | common_name required | | 404 | PKI not enabled or CA not initialized | | 500 | Certificate issuance failed | **Security Notes:** - Private key is generated server-side and returned in response - Store the private key securely; it is not recoverable - The certificate can be used with nginx, HAProxy, or curl for mTLS --- ### GET /pki/certs List certificates (own certificates only). **Request (authenticated):** ```http GET /pki/certs HTTP/1.1 Host: localhost:5000 X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 ``` **Response (200 OK):** ```json { "certificates": [ { "serial": "00000000000000000000000000000001", "common_name": "alice", "fingerprint_sha1": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", "created_at": 1700000000, "expires_at": 1731536000, "status": "valid", "issued_to": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" } ], "count": 1 } ``` **Notes:** - Anonymous users receive an empty list - Users see certificates they issued or certificates that match their fingerprint --- ### POST /pki/revoke/{serial} Revoke a certificate by serial number. Requires authentication and ownership. **Request:** ```http POST /pki/revoke/00000000000000000000000000000001 HTTP/1.1 Host: localhost:5000 X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 ``` **Response (200 OK):** ```json { "message": "Certificate revoked", "serial": "00000000000000000000000000000001" } ``` **Errors:** | Code | Description | |------|-------------| | 401 | Authentication required | | 403 | Permission denied (not owner) | | 404 | Certificate not found or PKI not enabled | | 409 | Certificate already revoked | **Authorization:** - Must be authenticated - Can revoke certificates you issued (issued_to matches your fingerprint) - Can revoke your own certificate (fingerprint matches) --- ### GET /register/challenge Get a proof-of-work challenge for public certificate registration. Returns a challenge with higher difficulty than paste creation. **Request:** ```http GET /register/challenge HTTP/1.1 Host: localhost:5000 ``` **Response (PoW disabled):** ```json { "enabled": false, "difficulty": 0 } ``` **Response (PoW enabled):** ```json { "enabled": true, "nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8", "difficulty": 24, "expires": 1700000300, "token": "a1b2c3d4...:1700000300:24:signature" } ``` **Notes:** - Registration difficulty defaults to 24 bits (vs 20 for paste creation) - Higher difficulty protects against automated certificate harvesting - Configurable via `FLASKPASTE_REGISTER_POW` environment variable --- ### POST /register Register for a client certificate (public endpoint with PoW protection). This endpoint allows anyone to obtain a client certificate for authentication. The CA is auto-generated on first registration if it doesn't exist. **Request (with PoW):** ```http POST /register HTTP/1.1 Host: localhost:5000 Content-Type: application/json X-PoW-Token: a1b2c3d4...:1700000300:24:signature X-PoW-Solution: 12345678 {"common_name": "alice"} ``` **Request (PoW disabled):** ```http POST /register HTTP/1.1 Host: localhost:5000 Content-Type: application/json {"common_name": "bob"} ``` **Response (200 OK):** ```http HTTP/1.1 200 OK Content-Type: application/x-pkcs12 Content-Disposition: attachment; filename="client.p12" X-Fingerprint-SHA1: b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3 X-Certificate-Expires: 1731533400 ``` **Response Headers:** | Header | Description | |--------|-------------| | `X-Fingerprint-SHA1` | SHA1 fingerprint for `X-SSL-Client-SHA1` header | | `X-Certificate-Expires` | Unix timestamp when certificate expires | **PKCS#12 Bundle Contents:** - Client certificate (signed by CA) - Client private key (EC secp384r1) - CA certificate (for trust chain) **Errors:** | Code | Description | |------|-------------| | 400 | Proof-of-work required (when enabled) | | 400 | Proof-of-work failed (invalid/expired challenge) | | 500 | PKI_CA_PASSWORD not configured | | 500 | Certificate generation failed | **Configuration:** ```bash export FLASKPASTE_REGISTER_POW=24 # Registration PoW difficulty (0=disabled) export FLASKPASTE_PKI_CA_PASSWORD="..." # Required for certificate signing export FLASKPASTE_PKI_CERT_DAYS=365 # Client certificate validity export FLASKPASTE_PKI_CA_DAYS=3650 # CA certificate validity (auto-generated) ``` **Using the Certificate:** ```bash # Extract certificate and key from PKCS#12 openssl pkcs12 -in client.p12 -clcerts -nokeys -out client.crt openssl pkcs12 -in client.p12 -nocerts -nodes -out client.key # Use with curl curl --cert client.crt --key client.key https://paste.example.com/ # Use fingerprint for header-based auth (behind reverse proxy) curl -H "X-SSL-Client-SHA1: $(openssl x509 -in client.crt -fingerprint -sha1 -noout | cut -d= -f2 | tr -d :)" \ https://paste.example.com/pastes ``` **Notes:** - `common_name` is optional; a random UUID is generated if omitted - The PKCS#12 bundle has no password (empty password) - CA is auto-generated on first registration if not present - Private key is generated server-side and included in response --- ### POST /csr Sign a Certificate Signing Request (CSR). Unlike `/register`, this endpoint allows clients to keep their private keys local. **How it works:** 1. Client generates a key pair locally 2. Client creates a CSR with their public key 3. Client submits CSR to server (with PoW if enabled) 4. Server signs CSR and returns the certificate 5. Client combines their private key with the signed certificate **Request (with PoW):** ```http POST /csr HTTP/1.1 Host: localhost:5000 Content-Type: application/x-pem-file X-PoW-Token: a1b2c3d4...:1700000300:24:signature X-PoW-Solution: 12345678 -----BEGIN CERTIFICATE REQUEST----- MIIBhDCB7gIBADBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcM ... -----END CERTIFICATE REQUEST----- ``` **Request (PoW disabled):** ```http POST /csr HTTP/1.1 Host: localhost:5000 Content-Type: application/x-pem-file -----BEGIN CERTIFICATE REQUEST----- ... -----END CERTIFICATE REQUEST----- ``` **Response (200 OK):** ```http HTTP/1.1 200 OK Content-Type: application/x-pem-file Content-Disposition: attachment; filename="alice.crt" X-Fingerprint-SHA1: b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3 X-Certificate-Expires: 1731533400 X-Certificate-Serial: 00000000000000000000000000000002 X-Is-Admin: 0 -----BEGIN CERTIFICATE----- MIICxDCCAaygAwIBAgIUY... -----END CERTIFICATE----- ``` **Response Headers:** | Header | Description | |--------|-------------| | `X-Fingerprint-SHA1` | SHA1 fingerprint for `X-SSL-Client-SHA1` header | | `X-Certificate-Expires` | Unix timestamp when certificate expires | | `X-Certificate-Serial` | Certificate serial number | | `X-Is-Admin` | `1` if first user (admin), `0` otherwise | **Errors:** | Code | Description | |------|-------------| | 400 | CSR required (empty body) | | 400 | Invalid CSR format | | 400 | CSR must contain a Common Name (CN) | | 400 | CSR signature is invalid | | 400 | Proof-of-work required (when enabled) | | 400 | Proof-of-work failed (invalid/expired challenge) | | 503 | PKI_CA_PASSWORD not configured | **Generating a CSR:** ```bash # Generate key pair and CSR with OpenSSL openssl ecparam -genkey -name secp384r1 -out client.key openssl req -new -key client.key -out client.csr -subj "/CN=alice" # Submit CSR to server curl -X POST --data-binary @client.csr \ -H "Content-Type: application/x-pem-file" \ https://paste.example.com/csr > client.crt # Verify the certificate openssl x509 -in client.crt -noout -subject -issuer ``` **Creating a PKCS#12 bundle:** ```bash # Combine key and certificate for browser/curl use openssl pkcs12 -export -out client.p12 \ -inkey client.key -in client.crt \ -certfile ca.crt -passout pass: # Use with curl curl --cert client.crt --key client.key https://paste.example.com/ ``` **Comparison with /register:** | Feature | /register | /csr | |---------|-----------|------| | Private key | Generated server-side | Generated client-side | | Response format | PKCS#12 bundle | PEM certificate only | | Key security | Transmitted over network | Never leaves client | | Complexity | Simple (one step) | More steps required | **Notes:** - Uses same PoW difficulty as `/register` (`FLASKPASTE_REGISTER_POW`) - CA is auto-generated on first CSR if not present - First user (via `/register` or `/csr`) becomes admin - CSR must be signed (server validates signature) - Common Name (CN) in CSR becomes certificate subject --- ## Audit Logging FlaskPaste logs PKI certificate lifecycle events for compliance and forensics. **Logged Events:** | Event | Trigger | Details | |-------|---------|---------| | `cert_issued` | Certificate registration or issuance | Type, CN, fingerprint, expiry | | `cert_revoked` | Certificate revocation | Serial, fingerprint | | `auth_failure` | Revoked/expired certificate used | Fingerprint, reason | **Log Format (production):** ```json { "time": "2024-12-24T10:30:00", "level": "INFO", "logger": "app.audit", "event": "cert_issued", "outcome": "success", "client_id": "a1b2c3d4...", "client_ip": "192.168.1.100", "details": {"type": "registration", "common_name": "alice"} } ``` **Notes:** - Audit logs are written to stdout in JSON format (production mode) - Events include client IP and certificate fingerprint for traceability - AUTH_FAILURE events are logged when revoked/expired certificates are used