Skip to content

🔐 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

✅ DO: 1. Binary Signature Check - Sign binaries with code signing certificate - Verify signature on startup

  1. License File Checksum
  2. RSA signature (described above)
  3. Detect manual editing

  4. Detect Common Cracks

  5. Check for known patcher signatures
  6. Detect debugger (basic check only)
  7. 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()}
        };
    }
};