Files
micro-license-server/README.md

152 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Micro License Server
A very small, self-hosted license server intended for lower traffic projects.
This license server has no concept of separate devices, so use only if you don't intend to restrict the amount of devices that a license key can be used on.
## Environment Setup
### 1. Clone the repository
```bash
git clone https://git.lexza.ch/Lexzach/micro-license-server.git
```
### 2. Create a new `.env` file in the cloned repository with the following contents:
```env
POSTGRESQL_PASSWORD="PG_CHANGEME"
API_KEY="API_CHANGEME"
NUM_KEY_CHUNKS=5
KEY_CHUNK_LENGTH=5
SIGN_KEY=true
```
### 3. Replace the placeholder secrets in `.env` with secure random values using `openssl rand`:
```bash
sed -i \
-e "s/PG_CHANGEME/$(openssl rand -hex 32)/" \
-e "s/API_CHANGEME/$(openssl rand -hex 32)/" \
.env
```
You can change `NUM_KEY_CHUNKS` to adjust how many chunks a license key consists of, and `KEY_CHUNK_LENGTH` to change how many characters appear in each chunk. This only changes how *new* keys are generated, so previous keys won't become invalidated. If `SIGN_KEY` is enabled the server will generate an Ed25519 key pair on first start and place it in the `keys/` directory (mounted into the container); subsequent restarts reuse those files.
### 4. Run `docker-compose up -d` in the cloned repository folder to start the server.
You will then have the server exposed on port 8000, if you need a different port, change it in the docker-compose file, then run `docker-compose up -d` again.
## API Endpoints
All authenticated endpoints expect a `Bearer` token via the `Authorization` header that matches `API_KEY` from `.env`. Unless stated otherwise, responses are JSON.
| Method | Path | Auth | Description | Key Request Parameters | Response Highlights |
| --- | --- | --- | --- | --- | --- |
| `GET` | `/` | No | Returns server metadata. | | `{"version": "Micro License Server vX.Y.Z"}` |
| `POST` | `/license` | Yes | Generates a new license key. | Query: `is_active` (bool, default `true`), `expiration_date` (ISO 8601, optional), `info` (string, optional metadata) | Unsigned response includes `license_key`, `expiration_timestamp`, `is_active`, `info`. With signing enabled the response becomes `{ "license": { "license_key", "expiration_timestamp" }, "signature" }`. |
| `GET` | `/is_valid` | No | Validates a license key and records last usage time. | Query: `license_key` (required) | Always returns `{ "valid": bool }`; with signing enabled and valid key also includes `license` payload and `signature`. |
| `POST` | `/license/{license_key}/disable` | Yes | Deactivates a license key. | Path: `license_key` | `license_key`, `is_active: false` |
| `POST` | `/license/{license_key}/enable` | Yes | Reactivates a license key. | Path: `license_key` | `license_key`, `is_active: true` |
| `POST` | `/license/{license_key}/expiration` | Yes | Sets or clears a license key expiration. | Path: `license_key`; Query: `expiration_date` (ISO 8601 or omit to clear) | `license_key`, `expiration_timestamp` (nullable) |
| `GET` | `/license/export` | Yes | Exports license inventory as CSV ordered by issue time. | | CSV with `license_key,issue_timestamp,expiration_timestamp,info,is_active` |
| `GET` | `/public-key` | No (when signing enabled) | Returns the base64-encoded Ed25519 public key used for signatures. | | `{ "public_key": "..." }` |
| `GET` | `/history/export` | Yes | Exports audit history as CSV. | Query: `token` (optional substring filter) | CSV with `action,timestamp` |
## Usage Examples
Set your API key once for convenience:
```bash
API_KEY=$(grep API_KEY .env | cut -d'"' -f2)
BASE_URL=http://127.0.0.1:8000
```
Create a license with optional metadata:
```bash
curl -X POST "$BASE_URL/license?info=QA+test+license" \
-H "Authorization: Bearer $API_KEY"
```
Check whether a license key is valid:
```bash
curl "$BASE_URL/is_valid?license_key=PUT-LICENSE-HERE"
```
Disable and enable a license:
```bash
curl -X POST "$BASE_URL/license/PUT-LICENSE-HERE/disable" \
-H "Authorization: Bearer $API_KEY"
curl -X POST "$BASE_URL/license/PUT-LICENSE-HERE/enable" \
-H "Authorization: Bearer $API_KEY"
```
Update (or clear) the expiration timestamp:
```bash
curl -X POST "$BASE_URL/license/PUT-LICENSE-HERE/expiration" \
-H "Authorization: Bearer $API_KEY" \
-G --data-urlencode "expiration_date=$(date -u -Idate)T23:59:59Z"
curl -X POST "$BASE_URL/license/PUT-LICENSE-HERE/expiration" \
-H "Authorization: Bearer $API_KEY"
```
Download the license inventory as CSV:
```bash
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/license/export" -o license_export.csv
```
Download history entries filtered by a token:
```bash
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/history/export?token=PUT-LICENSE-HERE" \
-o history_export.csv
```
## Verifying Signed Responses
When `SIGN_KEY=true`, successful `/is_valid` responses include a signature covering the license payload. The `/public-key` endpoint exposes the Ed25519 public key as base64-encoded bytes. The example below shows how to verify a response in Python using the `ecdsa` package.
```python
import base64
import json
import requests
from ecdsa import Ed25519, VerifyingKey
BASE_URL = "http://127.0.0.1:8000"
LICENSE_KEY = "PUT-YOUR-LICENSE-HERE"
# Fetch and cache the base64 public key once.
public_key_b64 = requests.get(f"{BASE_URL}/public-key", timeout=5).json()["public_key"]
public_key_bytes = base64.b64decode(public_key_b64)
verifying_key = VerifyingKey.from_string(public_key_bytes, curve=Ed25519)
# Validate the license key.
validation = requests.get(
f"{BASE_URL}/is_valid",
params={"license_key": LICENSE_KEY},
timeout=5,
).json()
if not validation.get("valid"):
raise RuntimeError("License is not valid")
if "signature" not in validation:
raise RuntimeError("Signing is disabled on the server")
license_payload = validation["license"]
message = json.dumps(license_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
signature = base64.b64decode(validation["signature"])
# Raises ecdsa.BadSignatureError if verification fails.
verifying_key.verify(signature, message)
print("Signature verified")
```