# Donjon Platform — Licensing & Post-Quantum Cryptography Reference

**Owner Reference Document** — captures the complete licensing architecture,
cryptographic standards, key management process, and admin operations.

---

## 1. Cryptographic Architecture

### Dual-Signature Scheme (Belt and Suspenders)

| Algorithm | Standard | Type | Purpose |
|-----------|----------|------|---------|
| **ML-DSA-65** | NIST FIPS 204 | Post-Quantum (lattice-based) | Future-proof against quantum computing |
| **Ed25519** | RFC 8032 | Classical (elliptic curve) | Fast, widely deployed, battle-tested |

**Both signatures must verify** for a license to be accepted. This protects against:
- A future quantum break of Ed25519 (ML-DSA-65 still holds)
- Undiscovered weaknesses in ML-DSA (Ed25519 still holds)

### Legacy v1 (Deprecated)
- **HMAC-SHA256** with embedded shared key (`donjon-license-signing-key-v1`)
- **Inherently weak** — anyone who decompiles the binary can forge v1 licenses
- Exists solely as a migration bridge for pre-v7 installations
- **TODO:** Remove v1 support once all deployed licenses are upgraded to v2

---

## 2. Key Management

### Key Files (Generated by `donjon-license-admin.py keygen`)

| File | Content | Security |
|------|---------|----------|
| `keys/donjon-private-classical.pem` | Ed25519 private key (PEM/PKCS8) | chmod 0o600, NEVER distribute |
| `keys/donjon-public-classical.pem` | Ed25519 public key (PEM) | Safe to embed in product |
| `keys/donjon-private-pqc.bin` | ML-DSA-65 private key (raw binary) | chmod 0o600, NEVER distribute |
| `keys/donjon-public-pqc.bin` | ML-DSA-65 public key (raw binary) | Safe to embed in product |
| `keys/revoked.json` | Revocation list (license IDs) | Synced to Cloudflare Worker |

### Key Storage Rules

- Private keys live ONLY on an air-gapped or tightly controlled signing workstation
- Private keys are gitignored (never committed)
- Public keys are base64-encoded and embedded in `lib/licensing.py` (lines 78-122)
- After signing, PQC private key buffer is zeroed via `ctypes.memset` (secure memory wipe)
- Ed25519 private key reference is deleted immediately after signing

### Embedded Public Keys (Product Side)

```python
# lib/licensing.py — VERIFICATION ONLY, no private keys
_PUBLIC_KEY_CLASSICAL_B64 = "A1ISZgiW2Xzgqmb+Ke8HU5ScQzh9VK/U4s4nW+b4Qbg="  # Ed25519
_PUBLIC_KEY_PQC_B64 = "Rq4iSkl/4jvYJmx2Fc1Jrpr7ofQRRDvSm2Pqse..."           # ML-DSA-65 (truncated)
```

**Note:** Current keys are placeholder values — replaced during build/deployment with real keys
generated by the admin tool.

---

## 3. License Admin Tool (`tools/donjon-license-admin.py`)

### Security Warning
```
This tool performs private key operations (key generation, signing).
It must NEVER be distributed with the product binary, shipped in Docker
images, or committed to public repositories.
```

### Commands

#### `keygen [--force]`
Generates new ML-DSA-65 + Ed25519 keypair. Outputs base64 public keys for embedding in source.

#### `sign --tier <tier> --org <name> [options]`
Creates a dual-signed license file.

| Option | Description |
|--------|-------------|
| `--tier` | community, pro, enterprise, managed |
| `--org` | Organization name |
| `--expires` | ISO date (YYYY-MM-DD), omit for perpetual |
| `--fingerprint` | Machine fingerprint (from `fingerprint` command) |
| `--max-users` | Override user limit (managed tier) |
| `--max-clients` | Override client limit (managed tier) |
| `--output` | Output file path (default: license.json) |

**Signing Workflow:**
1. Validate inputs (tier, dates, fingerprint format)
2. Assemble license payload JSON
3. Canonicalize payload (`sort_keys=True`, no whitespace, excludes signatures)
4. Sign with Ed25519 → base64 signature
5. Sign with ML-DSA-65 → base64 signature (private key zeroed after)
6. Attach both signatures, write output file

#### `verify --license <path>`
Verifies both signatures, checks expiry, reports license metadata.

#### `revoke --license-id <id>`
Adds license ID to `keys/revoked.json`. Format: `DON-<year>-<5 hex chars>`.

#### `fingerprint`
Generates machine fingerprint for hardware binding. Components:
- MAC address (first NIC)
- Hostname
- Platform (OS + architecture)
- Processor string
- Machine ID (Linux `/etc/machine-id`, Windows `MachineGuid` registry)

Output: `sha256:<64 hex chars>`

---

## 4. License Format (v2)

```json
{
  "version": 2,
  "license_id": "DON-2026-A3F7B",
  "tier": "enterprise",
  "organization": "Acme Corp",
  "issued_at": "2026-02-19T12:00:00Z",
  "expires": "2027-02-19T12:00:00Z",
  "machine_fingerprint": "sha256:abcdef1234...",
  "features_override": {},
  "signatures": {
    "classical": "<base64 Ed25519 signature>",
    "pqc": "<base64 ML-DSA-65 signature>",
    "algorithm_classical": "Ed25519",
    "algorithm_pqc": "ML-DSA-65-FIPS204"
  }
}
```

### Canonical Payload Rules (for signing)
- Excludes the `signatures` key (can't sign over itself)
- `sort_keys=True` — field-order-independent
- `separators=(",", ":")` — no whitespace
- `ensure_ascii=True` — no encoding ambiguities
- **Any change to serialization logic invalidates ALL existing licenses**

---

## 5. Product-Side Verification (`lib/licensing.py`)

### Verification Flow
1. Load `data/license.json`
2. Guard: file size < 1 MiB
3. Parse JSON, extract `signatures` block
4. Rebuild canonical payload (same rules as signing)
5. Verify Ed25519 signature against embedded public key
6. Verify ML-DSA-65 signature against embedded public key
7. Check expiry date
8. Check machine fingerprint (if bound)
9. Check revocation list
10. **Any failure → fall back to community tier** (deny by default)

### Graceful Degradation
- If `dilithium_py` not installed → PQC verification unavailable (ML_DSA_65 = None)
- If `cryptography` not installed → classical verification unavailable
- Missing dependencies → community tier (safe default)

---

## 6. Tier Ladder

### Community (Free — No License Required)
- 7 core scanners (network, vulnerability, web, SSL, Windows, Linux, compliance)
- quick + standard depth only
- 16 targets max per scan
- 30-day scan history
- 500ms artificial throttle between checks
- CSV + JSON export only
- 3 compliance frameworks
- AI analysis: Pro feature (Community uses built-in template-based analysis; Pro connects a local model or BYO key, ungated)
- 1 user
- No: scheduled scans, alert routing, SMS, SSO, RBAC, custom branding

**Natural upgrade trigger:** "I found issues, now I need a client report / scheduled scans / deeper analysis."

### Pro (License Required)
- ALL 19 scanners, ALL depths, no throttle
- Unlimited targets, unlimited history
- ALL export formats (HTML, PDF, SARIF, XML, CSV, JSON)
- Unlimited compliance frameworks, unlimited AI
- 25 users
- Gains: scheduled scans, alert routing, SMS notifications
- No: SSO, RBAC, custom branding, multi-tenant

### Enterprise (License Required)
- Everything from Pro
- Unlimited users, unlimited API rate
- Gains: SSO, RBAC, custom branding, multi-tenant, audit log export, priority support

### Managed / MSSP (License Required)
- Everything from Enterprise
- Gains: client provisioning, client dashboards, consolidated rollup, bulk scan orchestration, cross-client reporting, usage metering API, license sub-allocation, per-client branding, scan template library
- Unlimited clients

---

## 7. Cloudflare Worker (License Distribution)

**Domain:** `license.donjonsec.com`
**Runtime:** Cloudflare Workers (Python)
**Storage:** Cloudflare KV (`LICENSE_KV` binding)

### Endpoints

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/` | Public | Health check |
| GET | `/api/v1/public-keys` | Public | Distribute verification keys |
| GET | `/api/v1/revoked` | Public | Revocation list for online checks |
| POST | `/api/v1/validate` | Public | Online license validation |
| GET | `/api/v1/license/:id` | Public | License download |
| POST | `/api/v1/generate` | Admin (`X-Admin-Key`) | Store pre-signed license |
| POST | `/api/v1/revoke` | Admin (`X-Admin-Key`) | Revoke a license |
| GET | `/api/v1/stats` | Admin (`X-Admin-Key`) | Issuance statistics |

### KV Keys

| Key | Content |
|-----|---------|
| `admin_api_key` | Secret for admin endpoint auth |
| `public_keys` | JSON with Dilithium/Ed25519 public keys |
| `revocation_list` | JSON array of revoked license IDs |
| `license:<id>` | Individual signed license JSON |
| `stats` | Issuance counters |

### Security Controls
- `secrets.compare_digest()` for constant-time admin key comparison
- 512 KiB max request body
- 10,000 max revocation entries
- Strict license ID regex: `^[a-zA-Z0-9_-]{8,128}$`
- Control character sanitization on all inputs
- Generic error messages (no internal detail leakage)
- CORS: currently `*` (TODO: restrict to donjonsec.com in production)

---

## 8. End-to-End License Lifecycle

```
OWNER WORKFLOW:

1. KEYGEN        donjon-license-admin.py keygen
                 → generates 4 key files in keys/
                 → outputs base64 public keys for embedding

2. EMBED KEYS    Copy public key base64 into lib/licensing.py
                 (lines 78-122) during build/deployment

3. SIGN LICENSE  donjon-license-admin.py sign \
                   --tier enterprise \
                   --org "Acme Corp" \
                   --expires 2027-02-19 \
                   --fingerprint sha256:abc...
                 → writes signed license.json

4. DISTRIBUTE    Upload license.json to Cloudflare Worker:
                 POST /api/v1/generate (with X-Admin-Key)
                 — OR —
                 Deliver license.json directly to customer

5. ACTIVATE      Customer places license.json in data/
                 — OR —
                 POST /api/v1/license/activate via REST API

6. VERIFY        Product validates dual signatures on startup
                 → tier unlocked, features enabled

7. REVOKE        donjon-license-admin.py revoke --license-id DON-2026-A3F7B
                 → updates keys/revoked.json
                 → POST /api/v1/revoke to sync with Worker
                 → product checks revocation list on next validation
```

---

## 9. Security Invariants

- [x] Private keys NEVER leave the signing workstation
- [x] Private key material zeroed in memory after use (`ctypes.memset`)
- [x] Public keys only in product binary (compromise = forgery, not key theft)
- [x] Any validation failure → community tier (deny by default)
- [x] No exception details in error messages (prevents information leakage)
- [x] File size guards on all reads (prevent memory exhaustion)
- [x] License ID format validated by regex before any operation
- [x] Canonical JSON serialization prevents signature-bypassing format tricks
- [x] Machine fingerprinting deters casual license sharing
- [x] v1 legacy HMAC key is documented as weak — migration bridge only

---
