351 lines
8.7 KiB
Markdown
351 lines
8.7 KiB
Markdown
# 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**
|
|
|
|

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