Merisar SDK

Embed Merisar directly in your code.

The SDK is two layers you drop into your project. Layer 1 is a one-line macro that marks functions for cloud-side virtualization. Layer 2 is a linkable C library of in-source security primitives: keyed integrity, self-check, debugger & VM detection, branchless tamper responses, constant-time helpers. Mix them.

Quick start#

Three files, three steps. In your project:

  1. Grab the headers (svp_mark.h, svp_guard.h) and the static lib (libsvpguard.a / svpguard.lib). See Install below for exact paths.
  2. Mark the function(s) you want virtualized. That's it for Layer 1.
  3. Optional: call Layer 2 helpers from your code for an extra belt-and-braces check.
// hello.c
#include "svp/sdk/svp_mark.h"
#include "svp/sdk/svp_guard.h"

uint64_t check_license(const char* key) {
    // Layer 2: branchless debugger response, folded into the result.
    // No visible "if (debugged)" branch for a reverser to NOP out.
    uint64_t h = svp_mac(key, strlen(key), K0, K1);
    h ^= svp_poison(SECRET_MASK, svp_debugger_present());
    return h;
}
SVP_VIRTUALIZE(check_license);   // Layer 1: cloud-side VM lowering

Compile normally (cc hello.c -lsvpguard -o hello.exe). Then POST the binary to the Merisar build service — the marker in .svpmark tells us which functions to virtualize. You get a protected binary back where check_license runs inside a per-build VM and the native-code layer-2 call stays as an extra fallback.

Layer 1 — Source annotations#

Mark the function in source. The protector reads the marker at build time and replaces the function body with a VM call to your virtualized bytecode. No manual.svpmeta files, no fragile offset tables.

SVP_VIRTUALIZE(fn)

Mark fn for full VM lowering.

uint64_t check_license(const char* key) { /* ... */ }
SVP_VIRTUALIZE(check_license);
  • Place at file scope, immediately after the function definition.
  • Function must have external linkage (not static).
  • The protector sizes the function from .pdata (framed) or a bounded decode-to-RET (leaf) — if neither works, the function is reported as a native fallback (not silently skipped).
SVP_OBFUSCATE(fn)

Mark fn for lighter obfuscation (reserved for future levels).

SVP_OBFUSCATE(small_helper);
  • Currently treated as a lower-strength virtualization request.
  • Use for hot-path functions where you want some protection without the full VM dispatch cost.

Install

Where files go in your project:

  • include/svp/sdk/svp_mark.h — the macros. C and C++ compatible, GCC/Clang/MSVC.
  • include/svp/sdk/svp_guard.h — the runtime API (Layer 2).
  • lib/libsvpguard.a (POSIX) or lib/svpguard.lib (MSVC) — the implementation.

No external dependencies. The macros emit descriptors into a dedicated .svpmark PE section — the protector scans it when you upload the binary.

Layer 2 — Runtime guard library#

A C-ABI linkable library. Call these directly from your own code — they're backed by the same primitives the virtualized functions use, so native and VM-side checks share one implementation. Zero dependencies.

Environment detection

int svp_debugger_present(void)

Returns 1 if the process is being debugged, else 0.

if (svp_debugger_present()) { /* ... */ }
  • Checks four documented signals: IsDebuggerPresent, CheckRemoteDebuggerPresent, PEB.BeingDebugged (direct read, survives API hooks), NtGlobalFlag heap-debug bits.
  • Self-directed only. Not debugger-blinding / anti-attach — just a signal you can respond to.
  • Treat as advisory: a stealth plugin can hide from all four. Weight it; prefer folding results via svp_poison() over hard gates.
int svp_in_vm(void)

Returns 1 if a hypervisor is present (CPUID), else 0.

int suspect_env = svp_in_vm();
  • Reads CPUID.1 ECX[31] (hypervisor-present) and the vendor leaf 0x40000000.
  • Advisory. Will false-positive on legitimate users running in VMs or on cloud workstations — never hard-gate on this alone.

Keyed integrity (SipHash-2-4)

uint64_t svp_mac(const void* data, size_t len, uint64_t k0, uint64_t k1)

Keyed MAC over a buffer.

uint64_t tag = svp_mac(blob, len, K0, K1);
  • SipHash-2-4. Fast on short messages, keyed (not plain checksum).
  • Derive K0/K1 at runtime where you can — a key embedded as a literal in the binary is recoverable like any other embedded constant.
int svp_verify(const void* data, size_t len, uint64_t k0, uint64_t k1, uint64_t expected)

Returns 1 iff svp_mac(data,len,k0,k1) == expected. Constant-time tag compare.

if (!svp_verify(blob, len, K0, K1, SEALED_TAG)) corrupt_state();
  • Use this to detect tampering of data you sealed at build time.
  • The tag comparison is constant-time (no timing side channel on the 8 bytes).
uint64_t svp_code_mac(const void* code, size_t len, uint64_t k0, uint64_t k1)

MAC a region of the process's own code — detect inline hooks / breakpoints / patches.

uint64_t h = svp_code_mac(&hot_fn, hot_len, K0, K1);
if (h != SEALED_FN_TAG) corrupt_state();
  • Same primitive as svp_mac. Measure the byte range of the function you sealed at build time.
  • For a reliable fn-size, bake it in via linker symbols or compute at build time with a tool — do not guess at runtime.

Branchless tamper response

uint64_t svp_poison(uint64_t mask, int tripped)

Branchless select: returns mask when tripped is nonzero, else 0.

// Fold a check into a value — no visible "if (bad)" branch to NOP out.
key ^= svp_poison(SECRET_MASK, svp_debugger_present());
  • The whole point: tampered code silently produces wrong results — no compare / branch for an attacker to find and skip.
  • Compose with svp_debugger_present / svp_in_vm / svp_verify to turn any boolean signal into a poisoned value.

Constant-time helpers

int svp_ct_equal(const void* a, const void* b, size_t len)

Constant-time byte compare. 1 if equal, 0 otherwise.

if (svp_ct_equal(user_mac, expected_mac, 32)) grant_access();
  • No early-out: runtime does not depend on where the first differing byte is.
  • Use whenever you compare secrets (MACs, tokens, HMAC digests). Never use memcmp for that.
void svp_secure_zero(void* p, size_t len)

Zero memory such that the compiler cannot elide the write.

svp_secure_zero(derived_key, sizeof derived_key);
  • Call on sensitive buffers after use — stack-local keys, decrypted material, scrub lists.
  • Does not help against swapping / core dumps / cold-boot — those need OS-level mitigations too.

Combining both layers#

The intended power move: virtualize a function (Layer 1) that itself calls Layer 2 primitives. The checks are then obfuscated inside the VM and backed by native self-defense, so an attacker can't neutralize the protection by diffing for a single fixed injection pattern.

#include "svp/sdk/svp_mark.h"
#include "svp/sdk/svp_guard.h"

uint64_t check_license(const char* key) {
    uint64_t h = svp_mac(key, strlen(key), K0, K1);
    h ^= svp_poison(TAMPER_MASK, svp_debugger_present());  // Layer 2 inside...
    return h;
}
SVP_VIRTUALIZE(check_license);                              // ...a Layer-1 virtualized fn

At build time Merisar lifts check_license into a per-build VM whose opcodes are permuted and whose bytecode is encrypted at rest. The svp_poison() call becomes a VM CALL_HOST that executes the same native primitive — so if an attacker patches around the native path, they still have to defeat the virtualized copy, and vice versa.

Keys & honest limits#

  • Keys passed as parameters. svp_mac / svp_verify take k0 / k1 as parameters so you control where they live. For the strongest posture derive them at runtime (e.g. from a server response, a user-provided key, or something hardware-attested) rather than embedding a compile-time constant. Any constant baked into the binary is recoverable — this is true of every protector including commercial ones.
  • Detection honesty. svp_debugger_present and svp_in_vm are speed bumps. A stealth plugin can defeat all four debug signals; a hypervisor can lie about CPUID. These are useful signals you fold into results, not reliable gates to block execution on.
  • Branchless responses beat booleans. Prefer result ^= svp_poison(mask, cond) over if (cond) corrupt_state(). The former leaves nothing for an attacker to pattern-match; the latter is one well-known NOP away from disabled.
  • The SDK is C-ABI by design. Call it from C, C++, Rust (via extern "C"), Go (via cgo), or any language with a C FFI. The macros in svp_mark.h are C-only but the marking step can also be replaced by an explicit .svpmeta section if your toolchain doesn't preserve the .svpmark one.

HTTP API#

Programmatic access for CI, SDK integrations, and anything that isn't a human clicking the dashboard. Every request authenticates with an API key you mint at /dashboard/api-keys.

Auth header: Authorization: Bearer svp_live_.... Keys carry scopes; build creation needs builds:create, listing and status need builds:read.

Create a build

POST /api/v1/builds

Register a build and get a short-lived GCS signed PUT URL. The server never touches your binary; you upload directly to object storage.

curl -X POST https://merisar.com/api/v1/builds \
  -H "Authorization: Bearer svp_live_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "inputFilename": "app.exe",
    "inputSize": 1457920,
    "profile": { "base": "medium" }
  }'

# → 201
# {
#   "buildId": "f1e2d3c4-...",
#   "status": "pending_upload",
#   "uploadUrl": "https://storage.googleapis.com/...",
#   "uploadMethod": "PUT",
#   "uploadContentType": "application/octet-stream",
#   "uploadMaxBytes": 1457920,
#   "uploadExpiresAt": "2026-10-09T18:40:00.000Z",
#   "commitUrl": ".../api/v1/builds/f1e2.../commit",
#   "statusUrl": ".../api/v1/builds/f1e2..."
# }
  • Size cap: 128 MiB. Larger inputs are rejected with 400.
  • Profile keys: base = light | medium | virtualized, optional seed = <hex>. See Layer 1 for which transforms each base runs.
  • The signed URL is single-use and bound to Content-Type: application/octet-stream + the exact Content-Length you declared.
PUT <uploadUrl>

Push the bytes directly to GCS. Nothing proxies through our Next.js server, so this scales independent of your binary size.

curl -X PUT "<uploadUrl>" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @app.exe
# → 200
POST /api/v1/builds/:id/commit

Tell Merisar the upload is complete. We verify the object landed, match the declared size, flip the row to queued, and enqueue the protector. Idempotent.

curl -X POST https://merisar.com/api/v1/builds/f1e2.../commit \
  -H "Authorization: Bearer svp_live_XXXXXXXXXXXX"

# → 200
# { "ok": true, "buildId": "f1e2...", "status": "queued" }

Poll a build

GET /api/v1/builds/:id

Status + metadata. Poll at 1–2 Hz while the build is in flight; stop once status is terminal.

curl https://merisar.com/api/v1/builds/f1e2... \
  -H "Authorization: Bearer svp_live_XXXXXXXXXXXX"

# → 200
# {
#   "id": "f1e2...",
#   "status": "succeeded",      // pending | queued | running | succeeded | failed
#   "inputFilename": "app.exe",
#   "inputSize": 1457920,
#   "outputSize": 1521664,
#   "durationMs": 8420,
#   "fingerprintHex": "a2c9...",
#   "reportText": "...",        // protector stdout, up to 32 KB
#   "errorText": null,
#   "createdAt": "...",
#   "startedAt": "...",
#   "completedAt": "...",
#   "downloadUrl": ".../api/builds/f1e2.../download"
# }
  • Terminal statuses: succeeded, failed, cancelled.
  • fingerprintHex is the R12 watermark — hand it to /api/forensics/identify on a recovered binary to trace a leak back to the issuing build.
GET /api/v1/builds

List up to 50 recent builds for this key's team, newest first.

curl https://merisar.com/api/v1/builds \
  -H "Authorization: Bearer svp_live_XXXXXXXXXXXX"

# → 200
# { "builds": [ { "id": "...", "status": "succeeded", ... }, ... ] }

Download the protected binary

GET /api/builds/:id/download

302 redirect to a short-lived GCS signed GET URL. The download comes directly from GCS; no proxying.

curl -L https://merisar.com/api/builds/f1e2.../download \
  -H "Authorization: Bearer svp_live_XXXXXXXXXXXX" \
  -o protected.exe
  • Only works once status === "succeeded"; otherwise returns 409.
  • Session cookies also work here — this is the same URL the dashboard "Download" button uses.
  • The signed URL expires in 5 minutes; refetch to download again.

Error envelope

Errors return { "error": "<message>" } with the appropriate HTTP status. Common codes:

  • 400 — invalid body (schema failure, size out of range, bad buildId format)
  • 401 — missing or malformed Authorization header, or key doesn't exist / was revoked
  • 403 — key is valid but lacks the required scope
  • 404 — build id doesn't exist, or belongs to a different team
  • 409 — build is not yet in a state that supports this action (e.g. commit before upload lands, or download before success)
  • 502 — upstream (Cloud Tasks / worker) rejected the enqueue

Ready to ship it hardened?

Create an account, grab an API key, and your first build is a POST away.