Documentation · SoftActivate Licensing

Working offline

This page covers two related topics: how validation works on a device that is offline day to day, and how to activate a device that has no network access at all.

Everyday offline validation

After a device activates, whether online or through the exchange described below, every startup validation happens entirely offline. The SDK checks the stored license against the device, checks the current date, and confirms the license has not been revoked, all without contacting the service. No network access happens while the license is current.

A machine that goes offline keeps working until its license lapses. When connectivity returns, the next validation renews the license silently. If the device cannot refresh its cached revocation list, that check is skipped rather than failed, so an offline customer is never penalized for being offline.

Air-gapped offline activation

Some machines never have network access at all. For these, activation itself happens through a manual exchange of small files instead of a live connection to the service.

This exchange is deliberately scoped to perpetual, non-floating licenses, and only on license templates that opt in to it. Floating licenses and subscriptions are refused, because both depend on a live connection: a floating license renews a lease, and a subscription rechecks payment.

Step 1: on the air-gapped machine, create the request

C++

#include "softactivate/sws_licensing.h"
#include <fstream>

auto client = sws::licensing::client::create(endpointUrl);
client->set_metadata("trusted_domain", trustedDomain);
client->set_vendor_id(vendorId);
client->set_license_key(customerLicenseKey);

// entirely local - no network: generates this device's key pair and the request
auto request = client->create_offline_request();

std::ofstream("activation-request.txt") << request->get_offline_request_file();
std::ofstream("license.pending") << request->get_pending_private_key();

// email activation-request.txt to the vendor's support address;
// KEEP license.pending on this machine - never email it

C#

using System.IO;
using SWS.Licensing;

var client = new LicensingClient(endpointUrl)
{
    VendorId = vendorId,
    LicenseKey = customerLicenseKey,
    TrustedDomain = trustedDomain
};

// entirely local - no network: generates this device's key pair and the request
OfflineActivationRequest request = client.CreateOfflineActivationRequest();

File.WriteAllText("activation-request.txt", request.RequestFile);
File.WriteAllText("license.pending", request.PendingPrivateKey);

// email activation-request.txt to the vendor's support address;
// KEEP license.pending on this machine - never email it

VB.NET

Imports System.IO
Imports SWS.Licensing

Dim client As New LicensingClient(endpointUrl) With {
    .VendorId = vendorId,
    .LicenseKey = customerLicenseKey,
    .TrustedDomain = trustedDomain
}

' entirely local - no network: generates this device's key pair and the request
Dim request As OfflineActivationRequest = client.CreateOfflineActivationRequest()

File.WriteAllText("activation-request.txt", request.RequestFile)
File.WriteAllText("license.pending", request.PendingPrivateKey)

' email activation-request.txt to the vendor's support address;
' KEEP license.pending on this machine - never email it

Two files result. activation-request.txt goes to the vendor. It is a small text file that contains no secrets, so it is safe to email or carry on a USB stick. license.pending stays on the machine: the vendor's reply can only be applied using this file, so it must never be emailed. Creating the request needs no network connection.

Step 2: the vendor issues the license

Email activation-request.txt to your vendor's support address. The vendor's operator issues the license from the license's page in the console (the Offline activation panel) and replies with a license file. (If you are the vendor reading this: the console guide covers the operator side.)

Step 3: import and validate

C++

#include "softactivate/sws_licensing.h"
#include <cstdio>
#include <fstream>
#include <sstream>

auto read_file = [](const char* path) {
    std::ifstream file(path);
    std::stringstream buffer;
    buffer << file.rdbuf();
    return buffer.str();
};

auto client = sws::licensing::client::create(endpointUrl);
client->set_metadata("trusted_domain", trustedDomain);
client->set_vendor_id(vendorId);
client->set_license_key(customerLicenseKey);

// merge the certificate the vendor emailed back with the pending key - still no network
auto license = client->import_offline_response(
    read_file("license.pending").c_str(),
    read_file("response-certificate.pem").c_str());

// validate like any stored license (locally), then persist and retire the pending key
auto params = client->create_license_validation_params();
params->set_license(license);

auto result = client->validate_license(params);
if (result->is_license_valid())
{
    std::ofstream("license.dat") << license->serialize();
    std::remove("license.pending");
}

C#

using System.IO;
using SWS.Licensing;

var client = new LicensingClient(endpointUrl)
{
    VendorId = vendorId,
    LicenseKey = customerLicenseKey,
    TrustedDomain = trustedDomain
};

// merge the certificate the vendor emailed back with the pending key - still no network
License imported = client.ImportOfflineActivationResponse(
    File.ReadAllText("license.pending"),
    File.ReadAllText("response-certificate.pem"));

// validate like any stored license (locally), then persist and retire the pending key
LicenseValidationResult result = await client.ValidateLicenseAsync(imported);
if (result.IsValid)
{
    File.WriteAllText("license.dat", (result.UpdatedLicense ?? imported).Serialize());
    File.Delete("license.pending");
}

VB.NET

Imports System.IO
Imports SWS.Licensing

Dim client As New LicensingClient(endpointUrl) With {
    .VendorId = vendorId,
    .LicenseKey = customerLicenseKey,
    .TrustedDomain = trustedDomain
}

' merge the certificate the vendor emailed back with the pending key - still no network
Dim imported As License = client.ImportOfflineActivationResponse(
    File.ReadAllText("license.pending"),
    File.ReadAllText("response-certificate.pem"))

' validate like any stored license (locally), then persist and retire the pending key
Dim result As LicenseValidationResult = Await client.ValidateLicenseAsync(imported)
If result.IsValid Then
    File.WriteAllText("license.dat", If(result.UpdatedLicense, imported).Serialize())
    File.Delete("license.pending")
End If

Importing checks that the reply matches this machine's pending file. A reply imported on the wrong machine fails cleanly with an invalid-argument error, rather than leaving a broken license behind. The license then validates normally and is stored like any other; delete license.pending once the license validates.

Validity, re-issuance, and revocation

  • An offline license is not lease-capped. It runs to the license's own end date, or for perpetual licenses, an extended validity spanning multiple years. Re-issuing it later uses the same exchange again.
  • An air-gapped machine cannot receive revocation updates until it goes through re-issuance. Keep that in mind when deciding which templates to allow for offline activation.
  • Running the export again after emailing the request creates a new pending file, and the earlier reply will no longer match it. Export once, then wait for the reply.