diff --git a/activator/readme.md b/activator/readme.md new file mode 100644 index 0000000..f852a89 --- /dev/null +++ b/activator/readme.md @@ -0,0 +1,351 @@ +# Force Monitor — License Activation Guide + +This document explains how the licensing system works and how to activate **Force Monitor** after the trial period. + +--- + +## 📋 Table of Contents + +1. [How Licensing Works](#how-licensing-works) +2. [Trial Period](#trial-period) +3. [Getting Your Machine Fingerprint](#getting-your-machine-fingerprint) +4. [Activating Your License](#activating-your-license) +5. [License File Format](#license-file-format) +6. [Troubleshooting](#troubleshooting) +7. [For Vendors: Generating Licenses](#for-vendors-generating-licenses) + +--- + +## How Licensing Works + +Force Monitor uses **Ed25519 cryptographic signatures** to verify licenses. The system supports: + +- **Trial mode** — free for a limited time (default: 7 days) +- **Paid license** — perpetual or time-limited, tied to a specific machine + +### Key Concepts + +| Term | Description | +|------|-------------| +| **Machine Fingerprint** | Unique hardware ID derived from your machine (MAC, motherboard serial, etc.) | +| **Public Key** | Embedded in the app; used to verify license signatures | +| **Private Key** | Kept secret by the vendor; used to sign licenses | +| **Signed License** | JSON file cryptographically signed by the vendor | + +--- + +## Trial Period + +When you first run Force Monitor, it automatically starts a **7-day trial**: + +```bash +# Check your current license status +curl http://localhost:8080/api/license/status +``` + +**Example response (trial active):** +```json +{ + "enabled": true, + "mode": "trial", + "locked": false, + "message": "trial active: 6 day(s) remaining", + "days_remaining": 6, + "trial_started_at": "2026-04-20T17:00:00Z", + "trial_expires_at": "2026-04-27T17:00:00Z", + "fingerprint_short": "A1B2C3D4-...", + "activation_configured": true +} +``` + +### Trial Expiration + +After the trial expires, if `require_after_trial: true` (default), the app will: +- Return **HTTP 403 Forbidden** on all data endpoints +- Show a license activation page at `http://localhost:8080/` +- Display `"trial expired; activation required"` + +--- + +## Getting Your Machine Fingerprint + +To request a license, you need your **machine fingerprint**. Get it via API or the web UI. + +### Method 1: API (Recommended) + +```bash +curl -s http://localhost:8080/api/license/request | python3 -m json.tool +``` + +**Example response:** +```json +{ + "app": "force_monitor", + "version": "1.0.0", + "generated_at": "2026-04-20T17:30:00Z", + "hostname": "press-control-01", + "platform": "linux/amd64", + "fingerprint": "A1B2C3D4E5F6789012345678901234567890ABCDEF...", + "fingerprint_short": "A1B2C3D4-E5F67890", + "components": [ + "machineid=1234567890abcdef...", + "product_uuid=ABCDEF12-3456-7890-ABCD-EF1234567890", + "hostname=press-control-01", + "mac=aa:bb:cc:dd:ee:ff" + ] +} +``` + +### Method 2: Web UI + +1. Open `http://localhost:8080/` in your browser +2. If no `static/index.html` exists, a fallback page shows your **fingerprint** and **license mode** +3. Click **GET /api/license/request** to see the full activation request + +--- + +## Activating Your License + +Once you receive a signed license from the vendor, activate it using one of these methods: + +### Method 1: Web UI (Easiest) + +1. Open `http://localhost:8080/` in your browser +2. Paste the signed license JSON into the textarea +3. Click **Activate license** + +![Activation UI](docs/activation-ui.png) + +### Method 2: API (cURL) + +```bash +# Save your license to a file +cat > license.json << 'EOF' +{ + "app": "force_monitor", + "license_id": "LIC-2026-001", + "customer": "Your Company Name", + "fingerprint": "A1B2C3D4E5F6789012345678901234567890ABCDEF...", + "issued_at": "2026-04-20T17:00:00Z", + "expires_at": "2027-04-20T17:00:00Z", + "features": [], + "signature": "base64_encoded_ed25519_signature..." +} +EOF + +# Activate via API +curl -X POST http://localhost:8080/api/license/activate \ + -H "Content-Type: application/json" \ + -d @license.json +``` + +**Success response:** +```json +{ + "status": "activated", + "license": { + "mode": "licensed", + "locked": false, + "message": "license active", + "customer": "Your Company Name", + "license_id": "LIC-2026-001", + "expires_at": "2027-04-20T17:00:00Z" + } +} +``` + +### Method 3: Direct File Placement + +You can also place the license file directly: + +```bash +# Copy the signed license to the license directory +cp license.json license/license.json + +# Restart the app +``` + +--- + +## License File Format + +A valid signed license is a JSON file with this structure: + +```json +{ + "app": "force_monitor", + "license_id": "LIC-2026-001", + "customer": "Your Company Name", + "fingerprint": "A1B2C3D4E5F6789012345678901234567890ABCDEF...", + "issued_at": "2026-04-20T17:00:00Z", + "expires_at": "2027-04-20T17:00:00Z", + "features": ["premium", "mqtt"], + "signature": "base64_encoded_ed25519_signature..." +} +``` + +### Field Descriptions + +| Field | Required | Description | +|-------|----------|-------------| +| `app` | ✅ | Must match `product_code` in config (`force_monitor`) | +| `license_id` | ✅ | Unique license identifier | +| `customer` | ✅ | Customer name | +| `fingerprint` | ✅ | Must match the machine fingerprint exactly | +| `issued_at` | ✅ | ISO 8601 timestamp when license was issued | +| `expires_at` | ❌ | Optional expiration date (omitted = perpetual) | +| `features` | ❌ | Optional feature flags array | +| `signature` | ✅ | Ed25519 signature of the payload, base64-encoded | + +--- + +## Troubleshooting + +### "trial state invalid or tampered" + +**Cause:** System clock was changed, or trial files were modified. + +**Fix:** +```bash +# Remove trial state and restart +rm license/trial_state.json +# Restart the app — a new trial will begin +``` + +### "license fingerprint does not match this machine" + +**Cause:** The license was generated for a different machine. + +**Fix:** Generate a new activation request on this machine and request a new license. + +### "invalid license signature" + +**Cause:** The license was not signed with the correct private key, or was corrupted. + +**Fix:** +- Verify the license file wasn't modified +- Ensure the vendor used the correct private key matching your `public_key_base64` + +### "no license public key configured" + +**Cause:** `license.public_key_base64` is missing or invalid in `config.yaml`. + +**Fix:** Add the vendor's public key to your config: +```yaml +license: + enabled: true + public_key_base64: "YOUR_BASE64_PUBLIC_KEY_HERE" +``` + +### Check Current Status + +```bash +curl -s http://localhost:8080/api/license/status | python3 -m json.tool +``` + +--- + +## For Vendors: Generating Licenses + +This repository includes helper functions for license generation. Use them in a **separate, private signing tool**. + +### 1. Generate Ed25519 Key Pair + +```go +package main + +import ( + "crypto/ed25519" + "encoding/base64" + "fmt" +) + +func main() { + publicKey, privateKey, err := ed25519.GenerateKey(nil) + if err != nil { + panic(err) + } + + fmt.Println("Public Key (base64):") + fmt.Println(base64.StdEncoding.EncodeToString(publicKey)) + + fmt.Println("\nPrivate Key (base64) — KEEP SECRET:") + fmt.Println(base64.StdEncoding.EncodeToString(privateKey)) +} +``` + +### 2. Sign a License + +Use the `SignLicenseWithPrivateKey` helper from `license.go`: + +```go +package main + +import ( + "encoding/json" + "fmt" + "time" +) + +func main() { + lic := SignedLicense{ + App: "force_monitor", + LicenseID: "LIC-2026-001", + Customer: "Customer Name", + Fingerprint: "A1B2C3D4E5F6789012345678901234567890ABCDEF...", + IssuedAt: time.Now().UTC().Format(time.RFC3339), + ExpiresAt: time.Now().AddDate(1, 0, 0).UTC().Format(time.RFC3339), + Features: []string{}, + } + + privateKeyBase64 := "YOUR_PRIVATE_KEY_BASE64" + + signedLic, err := SignLicenseWithPrivateKey(lic, privateKeyBase64) + if err != nil { + panic(err) + } + + out, _ := json.MarshalIndent(signedLic, "", " ") + fmt.Println(string(out)) +} +``` + +### 3. Distribute the Public Key + +Give customers the **public key** to add to their `config.yaml`: + +```yaml +license: + enabled: true + public_key_base64: "LLQ43Fle4nObHxmMQQsANvPUX5vDxx0TctpvQs+RI4s=" +``` + +--- + +## Configuration Reference + +```yaml +license: + enabled: true # Enable/disable licensing + trial_days: 7 # Trial period duration + require_after_trial: true # Lock app after trial expires + data_dir: license # Directory for license files + public_key_base64: "" # Ed25519 public key for verification + product_code: force_monitor # App identifier (must match license) +``` + +--- + +## API Reference + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/license/status` | GET | Get current license status | +| `/api/license/request` | GET | Get activation request (fingerprint) | +| `/api/license/activate` | POST | Submit signed license JSON | + +--- + +## License + +This project is proprietary software. Contact your vendor for licensing inquiries. \ No newline at end of file