Files
flaskpaste/documentation/api.md
Username 6da80aec76
Some checks failed
CI / Lint & Format (push) Failing after 16s
CI / Unit Tests (push) Has been skipped
CI / Memory Leak Check (push) Has been skipped
CI / SBOM Generation (push) Has been skipped
CI / Security Scan (push) Successful in 19s
CI / Security Tests (push) Has been skipped
CI / Advanced Security Tests (push) Has been skipped
docs: update for simplified MIME detection (v1.5.1)
2025-12-26 19:52:40 +01:00

29 KiB

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.

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:

GET /challenge HTTP/1.1
Host: localhost:5000

Response (PoW disabled):

{
  "enabled": false,
  "difficulty": 0
}

Response (PoW enabled):

{
  "enabled": true,
  "nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8",
  "difficulty": 20,
  "base_difficulty": 20,
  "elevated": false,
  "expires": 1700000300,
  "token": "a1b2c3d4...:1700000300:20:signature"
}

Response (PoW enabled, anti-flood active):

{
  "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:

GET /health HTTP/1.1
Host: localhost:5000

Response (200 OK):

{
  "status": "healthy",
  "database": "ok"
}

Response (503 Service Unavailable):

{
  "status": "unhealthy",
  "database": "error"
}

GET /

Returns API information and usage instructions.

Request:

GET / HTTP/1.1
Host: localhost:5000

Response:

{
  "name": "FlaskPaste",
  "version": "1.5.0",
  "endpoints": {
    "GET /": "API information",
    "GET /health": "Health check",
    "POST /": "Create paste",
    "GET /<id>": "Retrieve paste metadata",
    "GET /<id>/raw": "Retrieve raw paste content",
    "DELETE /<id>": "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:

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):

{
  "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):

{
  "error": "Authentication required"
}

Response (403 Forbidden - non-admin using all=1):

{
  "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):

POST / HTTP/1.1
Host: localhost:5000
Content-Type: application/octet-stream

<binary content>

Request (JSON):

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:

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:

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):

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:

POST / HTTP/1.1
Host: localhost:5000
Content-Type: text/plain
X-Paste-Password: secretpassword

Password protected content

Response (201 Created):

{
  "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/1.1 201 Created
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1700000060
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:

GET /abc12345 HTTP/1.1
Host: localhost:5000

Request (Password Protected):

GET /abc12345 HTTP/1.1
Host: localhost:5000
X-Paste-Password: secretpassword

Response (200 OK):

{
  "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:

GET /abc12345/raw HTTP/1.1
Host: localhost:5000

Request (Password Protected):

GET /abc12345/raw HTTP/1.1
Host: localhost:5000
X-Paste-Password: secretpassword

Response (200 OK):

HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: inline

<binary content>
  • 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:

DELETE /abc12345 HTTP/1.1
Host: localhost:5000
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Response (200 OK):

{
  "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:

# Paste expires in 1 hour
curl -H "X-Expiry: 3600" --data-binary @file.txt http://host/

Configuration:

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:

# 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:

POST / HTTP/1.1
Host: localhost:5000
Content-Type: text/plain
X-Paste-Password: mysecretpassword

Protected content here

Response (201 Created):

{
  "id": "abc12345",
  "url": "/abc12345",
  "raw": "/abc12345/raw",
  "mime_type": "text/plain",
  "created_at": 1700000000,
  "password_protected": true
}

Accessing protected paste:

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):

{
  "error": "Password required",
  "password_protected": true
}

Response (403 Forbidden):

{
  "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:

# 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):

{
  "error": "Duplicate content rate limit exceeded",
  "count": 3,
  "window_seconds": 3600
}

Configuration:

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:

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):

{
  "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:

export FLASKPASTE_REQUIRE_BINARY=1  # Reject plaintext (0=disabled)

Response (400 Bad Request):

{
  "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:

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:

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:

{
  "error": "Proof-of-work required",
  "hint": "GET /challenge for a new challenge"
}
{
  "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:

{
  "error": "Description of the error"
}

For size limit errors (413):

{
  "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=<request-id>]

Example:

# 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:

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:

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:

GET /pki HTTP/1.1
Host: localhost:5000

Response (PKI disabled):

{
  "enabled": false
}

Response (PKI enabled, no CA):

{
  "enabled": true,
  "ca_exists": false,
  "hint": "POST /pki/ca to generate CA"
}

Response (PKI enabled with CA):

{
  "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:

POST /pki/ca HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{"common_name": "My CA"}

Response (201 Created):

{
  "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:

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:

POST /pki/issue HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{"common_name": "alice"}

Response (201 Created):

{
  "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):

GET /pki/certs HTTP/1.1
Host: localhost:5000
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Response (200 OK):

{
  "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:

POST /pki/revoke/00000000000000000000000000000001 HTTP/1.1
Host: localhost:5000
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Response (200 OK):

{
  "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:

GET /register/challenge HTTP/1.1
Host: localhost:5000

Response (PoW disabled):

{
  "enabled": false,
  "difficulty": 0
}

Response (PoW enabled):

{
  "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):

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):

POST /register HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{"common_name": "bob"}

Response (200 OK):

HTTP/1.1 200 OK
Content-Type: application/x-pkcs12
Content-Disposition: attachment; filename="client.p12"
X-Fingerprint-SHA1: b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3
X-Certificate-Expires: 1731533400

<binary PKCS#12 data>

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:

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:

# 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

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):

{
  "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