Offline license activation
How software checks a license without internet: signed offline leases, signed license files, grace periods, clock tampering and what each one trusts.
C++
To validate a license key in a C++ application, call your license server at startup, verify the Ed25519 signature on its answer with a public key compiled into the binary, and only then trust the license status and features. Bind the activation to the device, keep a signed offline lease for outages and check downloads against the signed hash. This guide shows how with velsigil::Client, the C++17 SDK included with Velsigil, which uses libsodium for the cryptography.
Last updated
Status of the C++ SDK: it is included with Velsigil as source code, but unlike the other SDKs it has not yet been compiled and run in Velsigil’s own release testing. The examples on this page follow its public header, velsigil/client.hpp, and were not compiled for this guide. Build the SDK with your toolchain and run its test suite before you ship.
You need C++17 and CMake 3.16 or later. The SDK is always built as a static library, velsigil::velsigil, with three dependencies: libcurl for HTTPS, libsodium for Ed25519, SHA-256 and random numbers, and nlohmann/json, which stays private. The public header uses only the standard library. Declared platforms are Windows 10 and later (MSVC 2019 or later, or MinGW-w64), Linux (GCC 9 or Clang 10 and later) and macOS 10.15 and later.
The SDK is included with Velsigil as source code. It is not a vcpkg port; vcpkg only supplies its dependencies.
The SDK’s vcpkg.json manifest lists curl, libsodium and nlohmann-json, so vcpkg installs them while CMake configures the build:
cmake -S sdks/cpp -B build -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure
cmake --install build --prefix ./dist # optional: headers, library, CMake package
Without vcpkg, use your platform’s packages: on Debian and Ubuntu libcurl4-openssl-dev, libsodium-dev and nlohmann-json3-dev; on Fedora libcurl-devel, libsodium-devel and json-devel; on macOS, Homebrew’s libsodium and nlohmann-json (libcurl ships with macOS).
cmake -S sdks/cpp -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static
cmake --build build --config Release
With a static triplet you ship a single .exe without curl or libsodium DLLs. The SDK then switches to the static MSVC runtime (/MT), so your application must use the same runtime. vcpkg’s curl uses Schannel on Windows, so certificates are checked against the Windows certificate store.
# As a subdirectory (curl, libsodium and nlohmann-json found through your own vcpkg toolchain)
add_subdirectory(third_party/velsigil-cpp)
target_link_libraries(my_app PRIVATE velsigil::velsigil)
# Or, after cmake --install
find_package(velsigil 1.0 CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE velsigil::velsigil)
curl, libsodium and nlohmann/json are private link dependencies of the library, so your own code only includes <velsigil/client.hpp>.
Copy the API URL, product ID and public key from the product’s Integration tab and compile them into the binary.
#include <velsigil/client.hpp>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
std::string load_license_key(); // your code
void enable_pro(); // your code
int main() {
// The default store is in memory: persist the device secret and offline lease instead.
velsigil::ClientOptions options;
std::filesystem::path store_path = velsigil::FileStore::default_path("MyApp");
if (store_path.empty()) store_path = "velsigil-license.json";
options.store = std::make_shared<velsigil::FileStore>(store_path);
velsigil::Client client("https://licenses.example.com", // from the product's Integration tab
"<product id>",
"<Ed25519 public key, base64>", // compiled in
options);
if (!client.is_configured()) { // the constructor never throws
std::cerr << client.configuration_error() << '\n';
return 2;
}
velsigil::ValidateOptions validate_options;
validate_options.version = "1.2.0";
const velsigil::ValidationResult result =
client.validate_with_offline_fallback(load_license_key(), validate_options);
if (!result.ok) {
std::cerr << result.code << " - " << result.message << '\n';
return 1;
}
if (result.license) std::cout << result.license->plan << (result.offline ? " (offline)" : "") << '\n';
if (const auto days = result.days_until_expiry()) std::cout << *days << " days left\n";
if (result.has_feature("pro")) enable_pro();
}
No public member function throws. Invalid constructor arguments are recorded instead: is_configured() returns false, configuration_error() explains why, and every call returns invalid_configuration.
ok is true only for a verified, signed success or a verified, unexpired offline lease for this device, and ValidationResult also converts to bool. code is a stable string with constants in velsigil::codes, message is safe to display, and request_id is the server’s reference for support requests.
Branch on codes, never on message text: invalid_key means ask for the key again, license_expired means offer a renewal, device_limit_reached means a device has to be freed in the customer portal, and invalid_response means the answer failed verification and should be treated as hostile. has_feature() is false whenever ok is false, and days_until_expiry() is empty for lifetime licenses.
The default MemoryStore loses the device secret when the process exits, so the next run looks like a cloned device. FileStore writes JSON atomically: with mode 0600 on POSIX, and with a protected access list for the current user and SYSTEM on Windows. FileStore::default_path("MyApp") points to %LOCALAPPDATA%\MyApp\velsigil-license.json on Windows and returns an empty path when no per-user folder can be found. For the OS keychain, implement IStore (get, set, erase).
The hardware ID is a SHA-256 of the machine ID: MachineGuid from the 64-bit registry view on Windows, /etc/machine-id on Linux, IOPlatformUUID on macOS. In containers, where the machine ID can change, set ClientOptions::hwid to a stable value of 8 to 256 characters.
The default transport is libcurl with TLS peer and host verification on, no redirects, HTTP and HTTPS only, and a 1 MiB cap on responses. For a proxy or your own network stack, implement ITransport::post_json. A custom transport sees the request body, which contains the license key and device secret, so never log it.
validate_with_offline_fallback uses the stored lease only when online validation fails with network_error; signed refusals and unsigned HTTP errors are never overridden. The lease is a signed token bound to the product, this device’s hardware-ID hash and an expiry that is never later than the license’s own. In the C++ SDK, if the stored lease is expired or invalid when the fallback runs, the result stays network_error, with a message that says so.
Every request carries a timestamp. If your clock is too far from the server’s (300 seconds by default), the server answers with a signed clock_skew that includes its time; the SDK learns the offset and retries once with a new nonce. Offline checks use the local clock plus that offset, so winding the clock back can stretch a lease up to its expiry: keep lease hours short. See offline license activation.
const auto update = client.check_update("1.2.0");
if (update.ok && update.update && update.update->update_available) {
const auto link = client.get_download(license_key, update.update->latest_version);
if (link.ok && link.download) {
const auto saved = client.download_release(*link.download, "/tmp/myapp-update.zip");
if (saved.ok) install_update("/tmp/myapp-update.zip"); // size and SHA-256 verified
}
}
download_release streams to a temporary file next to the destination, aborts as soon as more bytes arrive than announced, and renames the file into place only when the size and SHA-256 from the signed grant match. Do not build local paths from the server’s file_name without sanitizing it.
A Client can be shared between threads: its configuration is immutable after construction, the clock offset is atomic, every request uses its own libcurl handle and nonce, and both built-in stores lock internally. Custom stores, transports and clocks must be thread-safe too if the client is shared. Use one FileStore instance per file within a process. Client is movable but not copyable.
Native code is harder to read than .NET or Python, but disassemblers and debuggers still make a license check easy to find and patch. Stripping symbols, link-time optimization, control-flow obfuscation and integrity checks raise the cost; none of them is a security boundary.
ValidationResult instead of one global boolean./guard:cf /DYNAMICBASE /HIGHENTROPYVA; GCC and Clang -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pie -Wl,-z,relro,-z,now.ctest runs velsigil_vector_tests, which classifies every envelope and lease in the shared sdks/test-vectors.json and reproduces the hardware-ID examples, and velsigil_client_tests, which drives the client against an in-process signing server with an injected transport and clock. With -DVX_BUILD_HTTP_TESTS=ON, the same flows also run over real HTTP with libcurl against a small Python mock server. Run them on every compiler and platform you ship.
is_configured() after construction and ok on every result.FileStore; the default memory store forgets the device secret on every restart.network_error, and downloads are checked against the signed size and SHA-256.Keep reading
How software checks a license without internet: signed offline leases, signed license files, grace periods, clock tampering and what each one trusts.
How license keys get shared, forged or replayed, and the defenses that work: signed answers, replay protection, hashed keys, device secrets, rate limits.
How hardware-locked licenses bind a key to a device: machine IDs per OS, why hardware IDs can be spoofed, device secrets, limits and customer resets.
Validate license keys in C# and .NET: signed server answers, device binding, offline leases and update checks for WinForms, WPF, console apps and Unity.
Early access
Early access is free. In exchange, we ask for your feedback while we prepare the paid launch. Spots are limited, and we reply to every request by email.
Early access ends when paid subscriptions launch; we will tell members by email before then. To keep using Velsigil after that, you need a subscription. The 30% discount applies to the first 12 months of either plan, paid monthly or yearly.
Opens your email app with a short template: company, what you sell, your platforms and languages, expected license volume and how you plan to deploy. You can also write to hello@velsigil.com.