Files
micro-license-server/README.md

6.1 KiB
Raw Permalink Blame History

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

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:

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:

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:

API_KEY=$(grep API_KEY .env | cut -d'"' -f2)
BASE_URL=http://127.0.0.1:8000

Create a license with optional metadata:

curl -X POST "$BASE_URL/license?info=QA+test+license" \
  -H "Authorization: Bearer $API_KEY"

Check whether a license key is valid:

curl "$BASE_URL/is_valid?license_key=PUT-LICENSE-HERE"

Disable and enable a license:

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:

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:

curl -H "Authorization: Bearer $API_KEY" \
  "$BASE_URL/license/export" -o license_export.csv

Download history entries filtered by a token:

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.

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