🔐 License Validation Design¶
🏗️ System Architecture¶
High-Level Flow¶
┌──────────────────────────────────────────────────────────────┐
│ Plugin Startup │
│ ↓ │
│ Check local license file (license.json) │
│ ↓ │
│ IF valid signature & fresh (< 30 days) │
│ → ✅ Allow full access │
│ │
│ ELSE IF expired or missing │
│ → 🌐 Attempt online validation │
│ ↓ │
│ IF online check succeeds │
│ → ✅ Update local license, allow access │
│ ELSE IF online check fails (network issue) │
│ → ⏰ Check grace period │
│ ↓ │
│ IF within 7-day grace │
│ → ⚠️ Allow with warning │
│ ELSE grace expired │
│ → 🎁 Switch to trial mode │
└──────────────────────────────────────────────────────────────┘
🔑 License File Format¶
Structure (license.json)¶
{
"version": 1,
"license_key": "XXXX-XXXX-XXXX-XXXX",
"email": "user@example.com",
"name": "John Doe",
"product": "AudioLab Pro",
"product_version": "1.0",
"license_type": "commercial",
"issued_at": "2025-01-15T10:30:00Z",
"expires_at": "2026-01-15T10:30:00Z",
"last_check": "2025-10-03T14:22:00Z",
"grace_until": "2025-10-10T14:22:00Z",
"machine_id": "a1b2c3d4e5f6",
"seat_id": 1,
"features": {
"max_instances": 16,
"oversampling": true,
"advanced_algorithms": true
},
"signature": "BASE64_ENCODED_RSA_SIGNATURE"
}
License Types¶
| Type | Description | Features |
|---|---|---|
trial |
30-day evaluation | Full features, time-limited |
commercial |
Full paid license | All features, seat-limited |
educational |
Student/teacher | All features, watermark on export |
nfr |
Not for resale | All features, no commercial use |
subscription |
Monthly/yearly | All features, must renew |
Signature Generation¶
Server-side (private key):
import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
def sign_license(license_data: dict, private_key: rsa.RSAPrivateKey) -> str:
# Create canonical JSON (sorted keys, no whitespace)
canonical = json.dumps(license_data, sort_keys=True, separators=(',', ':'))
# Sign with RSA-SHA256
signature = private_key.sign(
canonical.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return base64.b64encode(signature).decode('utf-8')
Client-side (public key embedded in binary):
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/sha.h>
#include <nlohmann/json.hpp>
class LicenseValidator {
public:
bool verifySignature(const nlohmann::json& license) {
// Extract signature
std::string signature_b64 = license["signature"];
std::vector<uint8_t> signature = base64Decode(signature_b64);
// Create canonical JSON (without signature field)
auto license_copy = license;
license_copy.erase("signature");
std::string canonical = license_copy.dump(-1, ' ', false,
nlohmann::json::error_handler_t::strict);
// Verify RSA-SHA256 signature
EVP_PKEY* pubkey = getEmbeddedPublicKey();
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, pubkey);
EVP_DigestVerifyUpdate(ctx, canonical.c_str(), canonical.length());
int result = EVP_DigestVerifyFinal(ctx, signature.data(), signature.size());
EVP_MD_CTX_free(ctx);
return result == 1;
}
private:
EVP_PKEY* getEmbeddedPublicKey() {
const char* pubkey_pem = R"(
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
)";
BIO* bio = BIO_new_mem_buf(pubkey_pem, -1);
EVP_PKEY* key = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
return key;
}
};
⚙️ Machine Fingerprinting¶
Privacy-Aware Approach¶
Collect ONLY: - CPU ID (if available) - MAC address (primary network adapter) - OS installation ID
DO NOT Collect: - Hard drive serial (changes on upgrade) - Username/hostname (privacy concern) - IP address (changes, privacy) - Detailed hardware specs (unnecessary)
Implementation¶
#include <string>
#include <sstream>
#include <iomanip>
#include <openssl/sha.h>
#ifdef _WIN32
#include <Windows.h>
#include <intrin.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
#endif
class MachineFingerprint {
public:
static std::string generate() {
std::stringstream ss;
// CPU ID
ss << getCpuId() << "|";
// Primary MAC address
ss << getMacAddress() << "|";
// OS install ID (Windows only)
#ifdef _WIN32
ss << getWindowsInstallId();
#endif
// Hash the combined string
return sha256(ss.str()).substr(0, 12); // First 12 chars
}
private:
static std::string getCpuId() {
#ifdef _WIN32
int cpuInfo[4] = {0};
__cpuid(cpuInfo, 1);
std::stringstream ss;
ss << std::hex << std::setfill('0')
<< std::setw(8) << cpuInfo[3]
<< std::setw(8) << cpuInfo[0];
return ss.str();
#else
return "generic_cpu";
#endif
}
static std::string getMacAddress() {
#ifdef _WIN32
IP_ADAPTER_INFO AdapterInfo[16];
DWORD dwBufLen = sizeof(AdapterInfo);
if (GetAdaptersInfo(AdapterInfo, &dwBufLen) == NO_ERROR) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (int i = 0; i < 6; i++) {
ss << std::setw(2) << (int)AdapterInfo[0].Address[i];
}
return ss.str();
}
#endif
return "00:00:00:00:00:00";
}
static std::string getWindowsInstallId() {
// Read MachineGuid from registry
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid
// Implementation omitted for brevity
return "windows_guid";
}
static std::string sha256(const std::string& input) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256_CTX sha256;
SHA256_Init(&sha256);
SHA256_Update(&sha256, input.c_str(), input.size());
SHA256_Final(hash, &sha256);
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
ss << std::setw(2) << (int)hash[i];
}
return ss.str();
}
};
⚠️ Anti-Tampering¶
Basic Protection (Recommended)¶
✅ DO: 1. Binary Signature Check - Sign binaries with code signing certificate - Verify signature on startup
- License File Checksum
- RSA signature (described above)
-
Detect manual editing
-
Detect Common Cracks
- Check for known patcher signatures
- Detect debugger (basic check only)
- Log suspicious activity (analytics)
❌ DON'T: 1. Obfuscation arms race (waste of time) 2. Anti-debugging (breaks legitimate debugging) 3. VM detection (breaks studio workflows) 4. Aggressive responses (crashes, data loss)
Tamper Detection Implementation¶
class TamperDetection {
public:
static bool detectCommonCracks() {
bool suspicious = false;
// Check for known patcher files
if (fileExists("crack.dll") || fileExists("keygen.exe")) {
suspicious = true;
}
// Check for common debugger (passive detection)
#ifdef _WIN32
if (IsDebuggerPresent()) {
// Note: This also triggers for legitimate debugging
// Only log, don't block
logAnalytics("debugger_detected", false);
}
#endif
// Check binary integrity (optional)
if (!verifyBinarySignature()) {
suspicious = true;
}
if (suspicious) {
// DON'T crash or block - just log anonymously
logAnalytics("tamper_detected", false);
}
return suspicious;
}
private:
static void logAnalytics(const std::string& event, bool identifiable) {
// Send anonymous analytics (if user opted in)
// NO personally identifiable information
// Example: {"event": "tamper_detected", "version": "1.0.1", "os": "Windows"}
}
static bool verifyBinarySignature() {
#ifdef _WIN32
// Verify Authenticode signature
// Implementation using WinVerifyTrust API
#endif
return true; // Simplified
}
};
Anonymous Reporting¶
{
"event": "license_validation_failed",
"reason": "invalid_signature",
"version": "1.0.1",
"os": "Windows 10",
"timestamp": "2025-10-03T14:22:00Z",
"session_id": "random_uuid"
}
NO personal data: - ❌ Email, name, license key - ❌ IP address (use aggregated region only) - ❌ Machine ID - ✅ Version, OS, error type (for debugging)
🔄 Validation Flow Details¶
Startup Validation¶
enum class LicenseState {
Valid,
Trial,
Expired,
Invalid,
GracePeriod
};
class LicenseManager {
public:
LicenseState validate() {
// Step 1: Load local license
auto license = loadLicenseFile();
if (!license.has_value()) {
return checkTrialEligibility();
}
// Step 2: Verify signature
if (!verifySignature(license.value())) {
return LicenseState::Invalid;
}
// Step 3: Check expiration
if (isExpired(license.value())) {
return LicenseState::Expired;
}
// Step 4: Check last online validation
auto last_check = license.value()["last_check"];
auto now = getCurrentTime();
if (daysBetween(last_check, now) > 30) {
// Need online check
if (performOnlineCheck(license.value())) {
return LicenseState::Valid;
} else {
// Online check failed - enter grace period
return checkGracePeriod(license.value());
}
}
return LicenseState::Valid;
}
private:
LicenseState checkGracePeriod(const nlohmann::json& license) {
auto grace_until = license["grace_until"];
auto now = getCurrentTime();
if (now < grace_until) {
showGraceWarning(daysBetween(now, grace_until));
return LicenseState::GracePeriod;
} else {
return LicenseState::Expired;
}
}
void showGraceWarning(int days_remaining) {
std::cout << "⚠️ Could not validate license online.\n"
<< "Grace period: " << days_remaining << " days remaining.\n"
<< "Please connect to the internet to revalidate.\n";
}
};
📊 Monitoring & Analytics¶
What to Track¶
✅ Useful Metrics: - Activation success/failure rates - Grace period usage frequency - Offline activation usage - License transfer requests - Common error types
❌ Don't Track: - Individual user behavior - Project files or audio data - Plugin settings or presets - Detailed system information
Example Analytics Event¶
struct AnalyticsEvent {
std::string event_type;
std::string version;
std::string os_type;
std::string error_code; // If applicable
bool success;
nlohmann::json toJson() const {
return {
{"event", event_type},
{"version", version},
{"os", os_type},
{"error", error_code},
{"success", success},
{"timestamp", getCurrentISO8601Time()}
};
}
};
🔗 Related Documents¶
- LICENSING_PHILOSOPHY.md - Principles and strategy
- ACTIVATION_FLOW.md - User activation flows
- OpenSSL documentation for RSA signatures
- GDPR compliance guidelines for license data