Guide · By Buğra Ömer Aydemir · Updated 7 November 2026
Verify a Carcha Seal lease in C# and C++
For a desktop app, use the SDK: Carcha Seal has SDKs for C++, .NET and Electron. A lease is a standard JSON Web Signature signed with Ed25519, so code in C# or C++ can also verify it itself, offline, for example on your own server. The two verifiers on this page check a lease in the same order, with the same failure reasons, as the SDK, and were tested against leases signed by Carcha Seal's own code. They are verification-only reference code, not an SDK: activation, check-ins, storing the lease and the other SDK features are yours to build (see what the SDK does that these do not).
What you need
- Your server's public keys from
/v1/keys: for each key itskidandx(the 32-byte Ed25519 public key, base64url). Ship the current and the next key in your app, so keys rotate without an app update. - libsodium for the Ed25519 check (
crypto_sign_verify_detached), in both languages. .NET’s ownSystem.Security.Cryptographyhas no Ed25519 (dotnet/runtime issue 63174), so the C# code calls libsodium; shiplibsodium.dll,libsodium.dyliborlibsodium.sowith your app. - For C++, nlohmann/json and C++17. For C#, .NET 8.
Getting a lease
Your app gets a lease from the client API: POST /v1/activate once when the customer enters a key, then
POST /v1/validate every few hours. Both answer with token, the lease. Save it, and check it with the code below at every start and
whenever the app is offline. Computers that never go online use the offline request and license files instead (POST /v1/offline/activate).
POST /v1/activate
{ "licenseKey": "LK-…", "product": "example-studio", "fingerprint": "<64 hex characters>", "appVersion": "2.4.0" }
The device fingerprint
fingerprint and the lease’s fp claim are the SHA-256 of licensekit:<product>:<raw machine id>, in lowercase hex.
The raw machine id is the one the SDK reads:
- Windows:
win:andMachineGuidfromHKLM\SOFTWARE\Microsoft\Cryptography, read in the 64-bit registry view. - macOS:
mac:andIOPlatformUUIDfrom theIOPlatformExpertDeviceregistry entry. - Linux:
linux:and the contents of/etc/machine-id(or/var/lib/dbus/machine-id), trimmed.
The raw id never leaves the computer; only the hash is sent. Both verifiers include the hash function.
C#
LeaseVerifier.Check returns valid, grace or the failure reason; Verify returns the claims for HasFeature.
// Verifies a Carcha Seal lease on the device, with no network call, in the same order and with the same failure
// reasons as the SDK (verifyLease in @carchalabs/seal-core). .NET's own System.Security.Cryptography has no Ed25519,
// so the signature check calls libsodium (crypto_sign_verify_detached): ship the libsodium library for each platform
// with your app (libsodium.dll, libsodium.dylib, libsodium.so).
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace CarchaSeal;
public sealed record LeaseResult(bool Ok, string? Reason, JsonElement? Claims);
public static class LeaseVerifier
{
public const string Algorithm = "Ed25519";
public const string Type = "licensekit-lease+jwt";
public const string Issuer = "licensekit";
public const long LeewaySeconds = 300;
static readonly Regex Base64Url = new("^[A-Za-z0-9_-]+$", RegexOptions.Compiled);
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
static extern int sodium_init();
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
static extern int crypto_sign_verify_detached(byte[] sig, byte[] m, ulong mlen, byte[] pk);
static LeaseVerifier()
{
if (sodium_init() < 0) throw new InvalidOperationException("libsodium could not be initialised");
}
/// <summary>
/// The device fingerprint hash the SDK sends and the lease carries in "fp": SHA-256 of
/// "licensekit:<product>:<raw machine id>", lowercase hex. The raw machine id is "win:" + MachineGuid
/// (HKLM\SOFTWARE\Microsoft\Cryptography, 64-bit registry view), "mac:" + IOPlatformUUID or "linux:" + the
/// contents of /etc/machine-id, trimmed.
/// </summary>
public static string FingerprintHash(string rawMachineId, string product) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"licensekit:{product}:{rawMachineId}"))).ToLowerInvariant();
/// <param name="keys">kid → 32-byte Ed25519 public key: the "kid" and base64url-decoded "x" of each key in the
/// server's /v1/keys. Ship the current and the next key in your app.</param>
/// <param name="now">Unix seconds.</param>
/// <param name="graceSeconds">0 for a strict check; the lease's "grc" to allow its offline grace.</param>
public static LeaseResult Verify(string token, IReadOnlyDictionary<string, byte[]> keys, string product, string fingerprintHash, long now, long graceSeconds = 0)
{
var parts = token.Split('.');
if (parts.Length != 3 || Array.Exists(parts, p => p.Length == 0)) return Fail("malformed");
var header = ParseSegment(parts[0]);
if (header is not { ValueKind: JsonValueKind.Object } h) return Fail("malformed");
if (Str(h, "alg") != Algorithm) return Fail("unsupported_algorithm");
if (Str(h, "typ") != Type) return Fail("wrong_type");
if (h.TryGetProperty("crit", out _)) return Fail("malformed"); // RFC 7515 4.1.11: no critical extensions are understood.
var kid = Str(h, "kid");
if (kid is null || !keys.TryGetValue(kid, out var key) || key.Length != 32) return Fail("unknown_key");
if (!Base64Url.IsMatch(parts[2])) return Fail("malformed");
var signature = FromBase64Url(parts[2]);
var signingInput = Encoding.ASCII.GetBytes($"{parts[0]}.{parts[1]}");
if (signature.Length != 64 || crypto_sign_verify_detached(signature, signingInput, (ulong)signingInput.Length, key) != 0) return Fail("bad_signature");
var parsed = ParseSegment(parts[1]);
if (parsed is not { ValueKind: JsonValueKind.Object } c || !IsLeaseClaims(c)) return Fail("malformed");
if (Str(c, "iss") != Issuer) return Fail("wrong_issuer", c);
if (Str(c, "aud") != product) return Fail("wrong_product", c);
if (Str(c, "fp") != fingerprintHash) return Fail("wrong_device", c);
// The same clock-skew leeway on both ends (RFC 7519 4.1.4, 4.1.5).
if (now + LeewaySeconds < Num(c, "nbf")) return Fail("not_yet_valid", c);
if (c.TryGetProperty("exp", out var exp) && now - LeewaySeconds >= exp.GetDouble()) return Fail("expired", c);
if (now >= Num(c, "rev") + graceSeconds) return Fail("revalidation_required", c);
return new LeaseResult(true, null, c);
}
/// <summary>The SDK's state: "valid", "grace" (past the check-in deadline, within the lease's offline grace),
/// or the failure reason.</summary>
public static string Check(string token, IReadOnlyDictionary<string, byte[]> keys, string product, string fingerprintHash, long now, long defaultGraceSeconds = 7 * 86400)
{
var strict = Verify(token, keys, product, fingerprintHash, now);
if (strict.Ok) return "valid";
if (strict.Reason != "revalidation_required" || strict.Claims is not { } c) return strict.Reason!;
var grace = c.TryGetProperty("grc", out var g) ? (long)g.GetDouble() : defaultGraceSeconds;
return Verify(token, keys, product, fingerprintHash, now, grace).Ok ? "grace" : "revalidation_required";
}
/// <summary>True when the lease (already verified) grants the feature.</summary>
public static bool HasFeature(JsonElement claims, string feature)
{
foreach (var f in claims.GetProperty("feat").EnumerateArray()) if (f.GetString() == feature) return true;
return false;
}
static LeaseResult Fail(string reason, JsonElement? claims = null) => new(false, reason, claims);
static JsonElement? ParseSegment(string segment)
{
if (!Base64Url.IsMatch(segment)) return null;
try { return JsonDocument.Parse(FromBase64Url(segment)).RootElement.Clone(); }
catch (Exception e) when (e is JsonException or FormatException) { return null; }
}
static byte[] FromBase64Url(string s)
{
var b = s.Replace('-', '+').Replace('_', '/');
return Convert.FromBase64String(b.PadRight(b.Length + (4 - b.Length % 4) % 4, '='));
}
static string? Str(JsonElement o, string name) =>
o.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
static double Num(JsonElement o, string name) => o.GetProperty(name).GetDouble();
static bool IsNumber(JsonElement o, string name) =>
o.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number && double.IsFinite(v.GetDouble());
static bool IsLeaseClaims(JsonElement c)
{
foreach (var name in new[] { "iss", "sub", "aud", "jti", "fp" }) if (Str(c, name) is null) return false;
foreach (var name in new[] { "iat", "nbf", "rev" }) if (!IsNumber(c, name)) return false;
if (c.TryGetProperty("exp", out _) && !IsNumber(c, "exp")) return false;
if (!c.TryGetProperty("feat", out var feat) || feat.ValueKind != JsonValueKind.Array) return false;
foreach (var f in feat.EnumerateArray()) if (f.ValueKind != JsonValueKind.String) return false;
if (c.TryGetProperty("cus", out var cus) && cus.ValueKind != JsonValueKind.String) return false;
foreach (var name in new[] { "grc", "mnt" })
if (c.TryGetProperty(name, out _) && (!IsNumber(c, name) || Num(c, name) < 0)) return false;
if (c.TryGetProperty("fk", out var fk))
{
if (fk.ValueKind != JsonValueKind.Object) return false;
foreach (var p in fk.EnumerateObject()) if (p.Value.ValueKind != JsonValueKind.String) return false;
}
if (c.TryGetProperty("epk", out var epk) && epk.ValueKind != JsonValueKind.String) return false;
return true;
}
}
C++
Header only. Call sodium_init() once at startup; check_lease returns valid, grace or the failure reason.
// Verifies a Carcha Seal lease on the device, with no network call, in the same order and with the same failure
// reasons as the SDK (verifyLease in @carchalabs/seal-core). Needs libsodium (Ed25519, SHA-256, base64url) and
// nlohmann/json. Call sodium_init() once at startup before using it.
#pragma once
#include <sodium.h>
#include <nlohmann/json.hpp>
#include <cmath>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace carcha_seal {
inline constexpr const char* kAlgorithm = "Ed25519";
inline constexpr const char* kType = "licensekit-lease+jwt";
inline constexpr const char* kIssuer = "licensekit";
inline constexpr double kLeewaySeconds = 300;
struct LeaseResult {
bool ok = false;
std::string reason; // empty when ok
std::optional<nlohmann::json> claims;
};
// kid -> 32-byte Ed25519 public key: the "kid" and base64url-decoded "x" of each key in the server's /v1/keys.
using KeySet = std::map<std::string, std::vector<unsigned char>>;
inline std::optional<std::vector<unsigned char>> from_base64url(const std::string& s) {
if (s.empty() || s.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") != std::string::npos) return std::nullopt;
std::vector<unsigned char> out(s.size());
size_t len = 0;
if (sodium_base642bin(out.data(), out.size(), s.data(), s.size(), nullptr, &len, nullptr, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) return std::nullopt;
out.resize(len);
return out;
}
// The device fingerprint hash the SDK sends and the lease carries in "fp": SHA-256 of
// "licensekit:<product>:<raw machine id>", lowercase hex. The raw machine id is "win:" + MachineGuid
// (HKLM\SOFTWARE\Microsoft\Cryptography, 64-bit registry view), "mac:" + IOPlatformUUID or "linux:" + the
// contents of /etc/machine-id, trimmed.
inline std::string fingerprint_hash(const std::string& raw_machine_id, const std::string& product) {
const std::string input = "licensekit:" + product + ":" + raw_machine_id;
unsigned char digest[crypto_hash_sha256_BYTES];
crypto_hash_sha256(digest, reinterpret_cast<const unsigned char*>(input.data()), input.size());
char hex[crypto_hash_sha256_BYTES * 2 + 1];
sodium_bin2hex(hex, sizeof hex, digest, sizeof digest);
return hex;
}
namespace detail {
inline std::optional<nlohmann::json> parse_segment(const std::string& segment) {
auto bytes = from_base64url(segment);
if (!bytes) return std::nullopt;
auto value = nlohmann::json::parse(bytes->begin(), bytes->end(), nullptr, false);
if (value.is_discarded()) return std::nullopt;
return value;
}
inline bool is_string(const nlohmann::json& o, const char* name) { return o.contains(name) && o[name].is_string(); }
inline bool is_number(const nlohmann::json& o, const char* name) {
return o.contains(name) && o[name].is_number() && std::isfinite(o[name].get<double>());
}
inline std::string str(const nlohmann::json& o, const char* name) { return is_string(o, name) ? o[name].get<std::string>() : std::string(); }
inline bool is_lease_claims(const nlohmann::json& c) {
if (!c.is_object()) return false;
for (auto name : {"iss", "sub", "aud", "jti", "fp"}) if (!is_string(c, name)) return false;
for (auto name : {"iat", "nbf", "rev"}) if (!is_number(c, name)) return false;
if (c.contains("exp") && !is_number(c, "exp")) return false;
if (!c.contains("feat") || !c["feat"].is_array()) return false;
for (const auto& f : c["feat"]) if (!f.is_string()) return false;
if (c.contains("cus") && !c["cus"].is_string()) return false;
for (auto name : {"grc", "mnt"}) if (c.contains(name) && (!is_number(c, name) || c[name].get<double>() < 0)) return false;
if (c.contains("fk")) {
if (!c["fk"].is_object()) return false;
for (const auto& item : c["fk"].items()) if (!item.value().is_string()) return false;
}
if (c.contains("epk") && !c["epk"].is_string()) return false;
return true;
}
} // namespace detail
// now: Unix seconds. grace_seconds: 0 for a strict check; the lease's "grc" to allow its offline grace.
inline LeaseResult verify_lease(const std::string& token, const KeySet& keys, const std::string& product,
const std::string& fingerprint_hash, double now, double grace_seconds = 0) {
auto fail = [](const char* reason, std::optional<nlohmann::json> claims = std::nullopt) { return LeaseResult{false, reason, std::move(claims)}; };
std::vector<std::string> parts;
size_t start = 0, dot;
while ((dot = token.find('.', start)) != std::string::npos) { parts.push_back(token.substr(start, dot - start)); start = dot + 1; }
parts.push_back(token.substr(start));
if (parts.size() != 3) return fail("malformed");
for (const auto& p : parts) if (p.empty()) return fail("malformed");
auto header = detail::parse_segment(parts[0]);
if (!header || !header->is_object()) return fail("malformed");
if (detail::str(*header, "alg") != kAlgorithm) return fail("unsupported_algorithm");
if (detail::str(*header, "typ") != kType) return fail("wrong_type");
if (header->contains("crit")) return fail("malformed"); // RFC 7515 4.1.11: no critical extensions are understood.
auto key = keys.find(detail::str(*header, "kid"));
if (!detail::is_string(*header, "kid") || key == keys.end() || key->second.size() != crypto_sign_PUBLICKEYBYTES) return fail("unknown_key");
auto signature = from_base64url(parts[2]);
if (!signature) return fail("malformed");
const std::string signing_input = parts[0] + "." + parts[1];
if (signature->size() != crypto_sign_BYTES ||
crypto_sign_verify_detached(signature->data(), reinterpret_cast<const unsigned char*>(signing_input.data()), signing_input.size(), key->second.data()) != 0)
return fail("bad_signature");
auto claims = detail::parse_segment(parts[1]);
if (!claims || !detail::is_lease_claims(*claims)) return fail("malformed");
const auto& c = *claims;
if (detail::str(c, "iss") != kIssuer) return fail("wrong_issuer", c);
if (detail::str(c, "aud") != product) return fail("wrong_product", c);
if (detail::str(c, "fp") != fingerprint_hash) return fail("wrong_device", c);
// The same clock-skew leeway on both ends (RFC 7519 4.1.4, 4.1.5).
if (now + kLeewaySeconds < c["nbf"].get<double>()) return fail("not_yet_valid", c);
if (c.contains("exp") && now - kLeewaySeconds >= c["exp"].get<double>()) return fail("expired", c);
if (now >= c["rev"].get<double>() + grace_seconds) return fail("revalidation_required", c);
return LeaseResult{true, "", c};
}
// The SDK's state: "valid", "grace" (past the check-in deadline, within the lease's offline grace), or the failure reason.
inline std::string check_lease(const std::string& token, const KeySet& keys, const std::string& product,
const std::string& fingerprint_hash, double now, double default_grace_seconds = 7 * 86400) {
auto strict = verify_lease(token, keys, product, fingerprint_hash, now);
if (strict.ok) return "valid";
if (strict.reason != "revalidation_required" || !strict.claims) return strict.reason;
double grace = strict.claims->contains("grc") ? (*strict.claims)["grc"].get<double>() : default_grace_seconds;
return verify_lease(token, keys, product, fingerprint_hash, now, grace).ok ? "grace" : "revalidation_required";
}
// True when the lease (already verified) grants the feature.
inline bool has_feature(const nlohmann::json& claims, const std::string& feature) {
for (const auto& f : claims["feat"]) if (f.get<std::string>() == feature) return true;
return false;
}
} // namespace carcha_seal
How they were tested
A script signed leases with Carcha Seal’s own code (@carchalabs/seal-core) and checked each one with the SDK’s verifier, the C# code
and the C++ code. All three returned the same result in all 21 cases: a valid lease, one signed with the next key, inside and past the offline grace, the
default grace when a lease names none, expired, expired within the five minutes of clock skew allowed, not yet valid, another device, another product,
another issuer, an unknown key, edited claims, alg: none, HS256, the wrong type, a critical header and malformed tokens. The C#
code ran on .NET 8 and the C++ code was built with GCC 13 and -Wall -Wextra, both on Linux.
What the SDK does that these do not
- Storing the lease encrypted: keep it with the operating system’s protection (DPAPI on Windows, the Keychain on macOS).
- The clock check: the SDK remembers the latest time it has seen and reports
clock_suspectwhen the clock moves back more than a day. Keep that time with the lease and compare. - Hardware components, the device key and feature-key decryption (
decrypt): these need the native module, which is packaged for Node.js and Electron today. - Check-ins: call
/v1/validateevery few hours and replace the stored lease with the new one.
A check that runs on the customer’s computer can be patched by someone who controls it; a signature stops forged and edited leases, not an edited program.
How offline verification works · The lease format · Ask about your app