Skip to main content

C++ SDK

The C++ SDK is a C ABI you drop into a Windows project and call from C, C++, or anything that can bind extern "C". Download the bundle from your application's SDK Integration tab in the dashboard.

Requirements

ItemValue
OSWindows only (uses windows.h, __rdtsc, __fastfail)
ToolchainMSVC (Visual Studio 2019+ or Build Tools with v142/v143)
Architecturex64 or x86
C++ standardC++17 or newer
LinkageStatic (compile the .cpp files into your target) or DLL via OBSIDIAN_BUILDING_DLL

The header exports symbols with __declspec(dllexport) when OBSIDIAN_BUILDING_DLL is defined at build time. Without that define it uses __declspec(dllimport), which is what you want for static linkage.

Configuration

Every call takes an ObsidianConfig pointer. Fill it once and reuse it.

typedef void (*obs_secret_provider)(char* out, size_t out_cap);

typedef struct {
const char* app_id;
const char* app_name;
const char* api_secret;
const char* app_version;
obs_secret_provider api_secret_provider;
} ObsidianConfig;
  • app_id: the api_key for your application from the SDK Integration tab, sent as api_key on /api/client/init.
  • app_name: a free-form label for your own code, never sent to the server.
  • api_secret: an optional cache field the SDK never reads.
  • app_version: sent on /init. If the app has force_update on, a mismatch is rejected with 426; otherwise the mismatch is logged but the session still issues.
  • api_secret_provider: the callback the SDK invokes to obtain the HMAC secret at the moment it is needed, and the only supported source of the secret at runtime.

The provider pattern exists so the raw secret never lives in a static buffer or a hard-coded string. Store it however you want (env variable, encrypted resource, decrypted at boot) and return it through out on demand.

static void SecretProvider(char* Out, size_t Cap) {
const char* Secret = LoadSecretFromWherever();
strncpy_s(Out, Cap, Secret, _TRUNCATE);
}

ObsidianConfig Cfg = {};
Cfg.app_id = "obs_pk_...";
Cfg.app_name = "MyApp";
Cfg.app_version = "1.0.0";
Cfg.api_secret_provider = &SecretProvider;
Prologue guard

The SDK hashes the first 32 bytes of api_secret_provider on every call. If an attacker overwrites the callback to leak the secret, the hash mismatches and __fastfail terminates the process. Do not hot-patch or trampoline this function in your own code.

Return values

Every v2 entry point returns int: 1 means success, 0 means failure. That covers obsidian_init_v2, obsidian_login_v2, obsidian_register_v2, obsidian_license_v2, the three heartbeat calls, and obsidian_verify_proof. On failure the out_err buffer holds a human-readable message; see error handling.

obsidian_init_library and obsidian_shutdown_library have a different contract described in Library lifecycle.

Call proofs

Every state-mutating call also writes an ObsidianCallProof. You must verify the proof before trusting the call.

typedef struct ObsidianCallProof {
int status;
uint8_t mac[12];
uint64_t nonce;
} ObsidianCallProof;

OBSIDIAN_API int obsidian_verify_proof(const ObsidianCallProof* p,
const ObsidianConfig* cfg,
const void* out_ptr,
size_t out_len);

The proof binds status, the raw output buffer bytes, a random nonce, and config-derived key material into a MAC. Anything short of obsidian_verify_proof returning 1 is a bad call, even if the SDK returned 1 and the output buffer looks reasonable. Attackers who patch out an if (result) check still have to forge the MAC, and forging it requires the runtime secret they do not have.

The rule: check the return value, verify the proof, treat the two as one atomic decision.

ObsidianAuthResult Result = {};
ObsidianCallProof Proof = {};
char Err[256] = {};

int Rc = obsidian_login_v2(&Cfg, Session, User, Pass, Hwid,
&Result, Err, sizeof(Err), &Proof);

if (Rc != 1 || obsidian_verify_proof(&Proof, &Cfg, &Result, sizeof(Result)) != 1) {
HandleFailure(Err);
return;
}

Auth result

obsidian_login_v2, obsidian_register_v2, and obsidian_license_v2 all write into ObsidianAuthResult.

typedef struct {
int level;
char username[128];
char expires_at[64];
} ObsidianAuthResult;

level is the integer tier from the license (higher tiers unlock premium features in your own app; check level >= N however you want). username is populated on user auth calls and empty on license-only calls. expires_at is an ISO-8601 UTC string, or empty if the license has no expiry.

Library lifecycle

Call once at startup, once at shutdown.

OBSIDIAN_API int obsidian_init_library(void);
OBSIDIAN_API void obsidian_shutdown_library(void);

obsidian_init_library primes internal state and the pinned server public key. Every other entry point fails until this succeeds. Calling it a second time is a no-op.

Session bootstrap

OBSIDIAN_API int obsidian_init_v2(const ObsidianConfig* cfg,
char* out_session, size_t session_cap,
char* out_err, size_t err_cap,
ObsidianCallProof* out_proof);

Posts to /api/client/init and writes an opaque session token to out_session. Store the token and pass it to every subsequent call in the session. See Client Endpoints for the wire format.

Common failures: application disabled, version mismatch (426 when force_update is on), and rate-limit rejections (429).

Auth calls

OBSIDIAN_API int obsidian_login_v2(const ObsidianConfig* cfg,
const char* session,
const char* username,
const char* password,
const char* hwid,
ObsidianAuthResult* out_result,
char* out_err, size_t err_len,
ObsidianCallProof* out_proof);

OBSIDIAN_API int obsidian_register_v2(const ObsidianConfig* cfg,
const char* session,
const char* username,
const char* password,
const char* license,
const char* hwid,
ObsidianAuthResult* out_result,
char* out_err, size_t err_len,
ObsidianCallProof* out_proof);

OBSIDIAN_API int obsidian_license_v2(const ObsidianConfig* cfg,
const char* session,
const char* license,
const char* hwid,
ObsidianAuthResult* out_result,
char* out_err, size_t err_len,
ObsidianCallProof* out_proof);

obsidian_register_v2 transparently calls /api/client/login on success so the user's last_login field is populated immediately. obsidian_license_v2 is license-only auth; the server records the activation under the pseudo-username license-only.

HWID

OBSIDIAN_API int obs_compute_hwid(char* out, size_t out_len, uint32_t* out_sources_mask);

Returns a stable device fingerprint derived from WMI, MAC addresses, MachineGuid, and CPU identifiers. out_sources_mask receives a bitmask of the sources that actually contributed. See HWID Lock for the full source list and the known caveats.

Heartbeats

Heartbeats prove the client process is still running and give the server a kill switch. If the app has require_heartbeat enabled, login_v2 and license_v2 fail with Heartbeat session required. Call /heartbeat/start first. until a heartbeat session is live.

OBSIDIAN_API int obsidian_heartbeat_start_v2(const ObsidianConfig* cfg,
const char* session,
const char* system_id,
char* out_token, size_t token_len,
int* out_interval,
char* out_err, size_t err_len,
ObsidianCallProof* out_proof);

OBSIDIAN_API int obsidian_heartbeat_beat_v2(const ObsidianConfig* cfg,
const char* session,
const char* current_token,
const char* system_id,
int beat_index,
const char* code_hash_or_null,
int64_t code_size_or_zero,
char* out_new_token, size_t token_len,
char* out_err, size_t err_len,
ObsidianCallProof* out_proof);

OBSIDIAN_API int obsidian_heartbeat_stop_v2(const ObsidianConfig* cfg,
const char* session,
const char* token,
char* out_err, size_t err_len,
ObsidianCallProof* out_proof);

The token rotates on every beat. Always copy out_new_token back into current_token before the next call, or the server rejects you as a replay.

  • beat_index: monotonic counter starting at 0, incremented by one per beat. Gaps or repeats look like replay.
  • code_hash_or_null: optional SHA-256 hex of your executable region. The server compares against a baseline and flags injection.
  • code_size_or_zero: byte count matching code_hash. Pass 0 when omitting the hash.
  • out_interval: seconds between beats as decided by the server. Default is 30 when the server does not specify.

Send the next beat within 2 * out_interval seconds. Miss the window and the server marks the session dead, and subsequent calls fail with Heartbeat session required. See Heartbeat for the full protocol.

Reference loop:

char Token[256];
int Interval = 30;
if (obsidian_heartbeat_start_v2(&Cfg, Session, SystemId,
Token, sizeof(Token),
&Interval, Err, sizeof(Err), &Proof) != 1) { return; }
if (obsidian_verify_proof(&Proof, &Cfg, Token, strlen(Token)) != 1) { return; }

int BeatIndex = 0;
while (Running) {
Sleep(Interval * 1000);
char NextToken[256];
if (obsidian_heartbeat_beat_v2(&Cfg, Session, Token, SystemId,
BeatIndex++, CodeHashHex, CodeSize,
NextToken, sizeof(NextToken),
Err, sizeof(Err), &Proof) != 1) { KillApp(); }
if (obsidian_verify_proof(&Proof, &Cfg, NextToken, strlen(NextToken)) != 1) { KillApp(); }
strncpy_s(Token, sizeof(Token), NextToken, _TRUNCATE);
}

obsidian_heartbeat_stop_v2(&Cfg, Session, Token, Err, sizeof(Err), &Proof);

Utilities

OBSIDIAN_API void obs_zero(void* p, size_t n);

A compiler-fence-safe memset-zero. Use it to wipe passwords, secrets, and session tokens as soon as you are done with them. The SDK uses it everywhere internally.

Error handling

Every failing call writes a UTF-8 string into out_err. The message is either an SDK-side status or the detail field extracted from the server response.

MessageMeaning
SDK not initialisedYou forgot to call obsidian_init_library.
no api_secretapi_secret_provider returned an empty string.
networkHTTP request never completed.
no server sigResponse was missing the Ed25519 signature. Reject and treat as tampered.
sigverifyResponse signature failed verification. Reject; the pubkey pin caught something.
bad responseServer returned 200 but the expected field was missing.
Invalid license keyLicense does not exist for this app.
License key already usedNon-user license already claimed.
License key is bannedLicense marked banned in the dashboard.
Invalid username or passwordLogin failed. Also returned for banned users and expired accounts (uniform message on purpose).
Username already takenRegister conflict.
Application disabledApp toggled off in the dashboard.
Too many failed attempts, try again later.Client rate-limited (429).
Heartbeat session required. Call /heartbeat/start first.require_heartbeat is on and no live heartbeat exists.
HWID mismatch. Key locked to another device.HWID lock rejected the request.
Do not retry blindly on sigverify

sigverify means the request completed but the response was not signed by the real server. Treat network, no server sig, and sigverify as hard failures. Auto-retrying on sigverify is exactly what an attacker running a MITM wants.

Build

Add the SDK sources to your project and link crypt32.lib, winhttp.lib, and bcrypt.lib. Those are the libraries obs_crypto and obs_http already reference.

Static linkage from the command line:

cl /std:c++17 /EHsc /O2 /MT ^
your_app.cpp *.cpp ^
/link crypt32.lib winhttp.lib bcrypt.lib /OUT:your_app.exe

The SDK bundle ships every translation unit obsidian_client.cpp depends on (obs_crypto.cpp, obs_http.cpp, obs_proof.cpp, obs_env.cpp, obs_pubkey.cpp, obs_secure_mem.cpp, obs_vmp.cpp, ed25519.cpp, and friends). Compile all of them, or you will hit unresolved-external errors on symbols like obs_env_init, obs_get_server_pubkey, obs_verify_pubkey_hash, obs::detail::ComputeCallProof, and ed25519_verify.

Both x64 and x86 build. Pick one and keep it consistent with the SDK bundle you downloaded.