RockyGuard

Documentation · AI Integration

AI Integration Guide

A tool-agnostic recipe written for AI coding assistants. You do not need to download anything first. Point Claude Code, Cursor, GitHub Copilot, ChatGPT, Gemini, or Cody at this site and say:

Please read the AI integration guide at https://rockyguard.dev and then integrate RockyGuard into my project.

The agent resolves the right build for your OS from /downloads/latest.json, asks your permission before downloading, verifies the SHA-256, extracts it, and asks for the inputs it needs (public key — it can generate the keypair for you — target binary, license-load path, tier and feature gates). It then performs the CMake + main() + feature-gate edits and runs your normal build to confirm. It finishes by reading your machine fingerprint with rg_fingerprint and asking whether to send Rocky Software a 30-day trial licence request — the vendor licence you need before license_create will mint anything.

Two steps are gated on an explicit yes from you, by rule: downloading the archive, and sending the licence request. The agent will not do either on its own. The raw markdown is at /AI_INTEGRATION_GUIDE.md, indexed from /llms.txt, and the same file ships at the root of every package.

The guide covers two ways to enforce a paid feature. The simple branch — verify, then refuse if the result is not OK — is a few lines, but it is one boolean a one-instruction binary patch can flip. Load-bearing binding instead seals the feature’s own data under a key the licence carries (rg_bind_asset + LicenseAsset), so a patched or forged licence just yields garbage rather than a working unlock. Use it for a high-value feature; leave the rest on the simple branch.

Customer-side description of this path: Customer Documentation §2.6. Same content as a downloadable file: AI_INTEGRATION_GUIDE.md.

RockyGuard Integration Guide for AI Assistants

You are an AI assistant (Claude, ChatGPT, Cursor, Copilot, Gemini, Cody, or similar) helping a software developer integrate the RockyGuard license library into their C++17 application.

Read this entire file before making any changes. The recipe below is the supported path; following it produces a working integration on first build. Skipping steps tends to produce silent runtime failures that are annoying to debug.

If you are a human reader: this file is a how-to. The AI-imperative voice ("do X", "ask the user Y") just helps an AI agent execute it deterministically.

Where to start. If you were pointed at https://rockyguard.dev and the user does not yet have the library on disk, start at Section 0.1 — it resolves the right build for their platform, verifies it, and extracts it. If you are reading this from the root of an already-extracted package (include/, lib/, tools/ and docs/ are its siblings), start at Section 1.

Canonical copy of this file: https://rockyguard.dev/AI_INTEGRATION_GUIDE.md. It is the same text that ships at the root of every package, and it is refreshed on every release — prefer it over a copy you were handed if the two disagree.


0. What you are integrating

RockyGuard is a C++17 static library that verifies signed license files at program startup. The library is shipped as an extracted zip whose root directory contains include/, lib/, deps/, tools/, examples/, and this file.

The integration adds four things to the user's project:

  1. A small block of CMake that links against <package>/lib/static/rockyguard.{lib,a}.
  2. A C++17 string constant containing the user's RSA/Ed25519 public key.
  3. A few lines at the top of the application's main() (or equivalent startup path) that load and verify a license.json file.
  4. For each paid feature: a load-bearing binding (§6.5) — the feature's own mandatory data, decrypted with a key the license yields, so a patched-out check produces garbage instead of a working unlock.

There is no daemon, no SaaS, no online activation. Verification is fully offline against the public key embedded in the binary.

Read this before choosing where to spend effort. An offline license check is, on its own, one branch on the attacker's CPU — flip if (!result) refuse; and the app runs. The startup verify in §5 is necessary but it is not where the protection lives; it drives user messaging, expiry, and hardware binding. The enforcement that actually resists patching is §6.5: binding each paid feature to a key derived from the license. Treat §6.5 as part of the required recipe wherever a feature has a bindable asset — not as optional hardening. Sections 9.1–9.4 and 9.6 are the genuinely optional extras on top.

Optionally -- and only if the user asks -- a fourth: a Help → Diagnose License dialog that reports what licensing is actually doing on an end user's machine. See Section 8. It is not part of the required recipe.


0.1 Get the package (start here if you do not already have it)

Skip this section if the user already has an extracted RockyGuard package. You will know: this file sits at its root, beside include/, lib/, tools/, deps/, cmake/ and docs/.

Otherwise work through the seven steps below, in order.

Do not improvise a download URL. The zip filenames carry the version number and rotate on every release, so any literal zip URL -- including one written in this sentence -- is wrong by the next one. Resolve the download from the manifest in step 2. Its URL is the only thing here that never moves.

Step 1 — Identify platform and architecture

Map the host to one of exactly three slugs:

Host Slug
Windows 10 / 11, x86-64 windows-x64
Linux, x86-64, glibc ≥ 2.34 (Ubuntu 22.04+, RHEL 9+, Debian 12+) linux-x64
macOS 13+, x86-64 macos-x64

The bundles are x86-64 only. There is no arm64 build.

  • Apple Silicon (arm64 macOS): macos-x64 runs under Rosetta 2, but the user's application must then be built for x86_64 too — you cannot link an x86_64 static library into an arm64 target. Stop and tell the user this before downloading, so they can decide whether an x86_64 toolchain is acceptable for their product.
  • arm64 Linux: unsupported. Stop and tell the user to contact https://rockyguard.dev/contact. Do not download the x64 bundle "to see if it works"; it cannot.

Determine glibc with ldd --version on Linux. On an older glibc, stop: the prebuilt librockyguard.a will link but fail at load time with unresolved versioned symbols, which is a confusing failure to hand a user.

Step 2 — Fetch the manifest

curl -fsSL https://rockyguard.dev/downloads/latest.json

It returns the current release. The fields you need:

Field Meaning
version e.g. 1.4.0. Quote it to the user.
platforms.<slug>.url The zip to download.
platforms.<slug>.sha256 Digest you must verify in step 5.
platforms.<slug>.bytes Exact size. Quote it when asking permission.
platforms.<slug>.root The directory name the zip extracts to.
license_request Where the 30-day Premium trial request goes (Section 10.5).

If the manifest is unreachable, or the slug you need is absent from platforms, stop and ask the user. Do not fall back to guessing a filename, and do not scrape https://rockyguard.dev/download for a link — that page is rendered from this same manifest, so a failure to fetch one is not fixed by fetching the other.

Step 3 — Ask the user before downloading

This is a hard rule (Section 11), not a courtesy. Downloading and then executing binaries from the network is the user's decision to make, and they are entitled to make it with the facts in front of them. State, in one message: the product and version, the URL, the size in bytes, the SHA-256 you are going to verify against, and where on disk you intend to put it.

Wait for an explicit yes. If the user declines, stop — do not offer a workaround, and do not proceed with a package from some other source.

Step 4 — Download

# Linux / macOS
curl -fL -o rockyguard.zip "<platforms.<slug>.url>"

# Windows PowerShell
Invoke-WebRequest -Uri "<platforms.<slug>.url>" -OutFile rockyguard.zip

Download to a scratch directory, not into the user's repository. Nothing enters their tree until step 6, after the digest has been checked.

Step 5 — Verify the SHA-256, and abort on mismatch

# Linux
sha256sum rockyguard.zip

# macOS
shasum -a 256 rockyguard.zip

# Windows PowerShell
(Get-FileHash -Algorithm SHA256 rockyguard.zip).Hash.ToLower()

Compare against platforms.<slug>.sha256 from the manifest.

On mismatch: delete the file, tell the user the digest did not match, and stop. Do not retry, do not extract it "to check", and do not continue with a note in the summary. A mismatch means the bytes are not the bytes we published, and there is no benign reading of that which justifies running the tools inside. The user can re-run later; a corrupted or substituted package integrated into their build is not something they can undo by re-running anything.

Step 6 — Extract into the repository

Confirm the destination with the user first — Section 2 covers the layout and the cases where you must ask (an existing third_party/ convention, submodules, vcpkg, Conan). The default is <their-repo>/vendor/rockyguard/.

Extract, then move the contents of the root directory (the manifest told you its name) up so the package root is vendor/rockyguard/. You want vendor/rockyguard/include/, not vendor/rockyguard/rockyguard-v1.4.0-linux-x64-demo/include/ — every CMake path in Section 3 assumes the former.

Then make the tools runnable:

  • Linux / macOS: the zips carry Unix mode bits, but some extractors drop them. Check ls -l vendor/rockyguard/tools/ and chmod +x vendor/rockyguard/tools/* if the exec bit is missing.
  • macOS additionally: the binaries are not notarized, so Gatekeeper will refuse to run them after a browser or curl download sets the quarantine attribute — the symptom is a "cannot be opened because the developer cannot be verified" dialog, or a bare killed: 9. Clearing it requires the user's informed consent, so tell them what the command does and why before running it:
    xattr -dr com.apple.quarantine vendor/rockyguard
    
    If the user would rather clear it per-binary through System Settings → Privacy & Security, that works too and is entirely reasonable.

Step 7 — Smoke-test, and surface the licence agreement

vendor/rockyguard/tools/rg_fingerprint --help

If that prints usage, the package is intact and the toolchain can run it. A missing-shared-library error on Linux usually means the glibc check in step 1 was skipped.

Then tell the user, in one sentence, that the package ships the RockyGuard Library Software License Agreement at vendor/rockyguard/docs/RockyGuard_License_Agreement.pdf (and .txt), and that the 30-day Premium trial does not grant the right to sell licences to paying end users — that needs Basic or Premium. Do not paraphrase the agreement's terms beyond that, and do not accept it on the user's behalf.

You now have everything Sections 1–10 need. Continue at Section 1.


1. Inputs the user must provide

Before writing any code, settle the following. Ask explicitly; do not guess. Items 2–4 are hard gates — halt until you have answers.

  1. Public key (PEM format, ~100 bytes for Ed25519, ~400 bytes for RSA).

    • If the user already has a keypair, this is a hard gate too: read the actual public.pem content from disk, or ask them to paste it. Do not invent or generate one to stand in for theirs.
    • If they do not have one yet, do not halt — generate it, after telling them you are about to:
      <package>/tools/license_keygen --algo ed25519
      
      This produces private.pem (stays on the user's build machine — never commit, never ship; add it to .gitignore per Section 11) and public.pem (embedded in the compiled binary in Section 4). Ed25519 is the right default: smaller signatures, smaller embedded key, and the verifier auto-detects the algorithm either way (Section 5). Choose RSA only if the user says they need it.
    • Confirm with the user that the key you are embedding is the public one. Embedding private.pem by mistake ships their signing key to every customer, and nothing downstream will warn you.
  2. Where the application's main() lives. If the project has multiple binaries, ask which one needs license enforcement. Default to the end-user-facing executable.

  3. Where they want license.json to be loaded from at runtime. The default is a per-user, user-writable location: %APPDATA%\<Company>\<Product>\license.json on Windows, $XDG_CONFIG_HOME/<Company>/license.json (falling back to ~/.config) on Linux. "Next to the executable" is acceptable only when the executable itself lives somewhere the running user can write (a portable or per-user install) -- never under %ProgramFiles% or /opt. Reason: the anti-rollback machinery writes a witness file (.lck_cache_<tag>) beside the license on every verification; when that directory is read-only the write fails silently, verification still passes, and clock-rollback protection is weaker on that machine for good (manual §3.4, §9.2). Confirm before deviating from the default, and say why when you do.

  4. Tier they have purchased, or are trialling.

    • Not sure: treat as Basic for the purposes of this guide, and skip Section 7. The library is not crippled — the tier is a licensing decision, not a build — so the whole recipe except floating licensing applies unchanged.
    • Basic: node-locked licensing only. Skip Section 7.
    • Premium: node-locked + floating-license server/client. Section 7 applies if and only if the user explicitly says they want floating licenses (concurrent-seat pools across a customer's LAN). Most Premium customers ship products that use only node-locked.
    • 30-day Premium trial: the trial vendor licence is Premium tier, so treat it as Premium above — Section 7 only if they explicitly ask for floating. If they do, say that keeping floating licensing after the trial needs Premium; Basic is node-locked only.
  5. Whether they already have a vendor licence. Ask, but do not halt on it — this is the one input that is allowed to arrive last.

    A vendor licence (vendor_license.json, issued by Rocky Software and bound to the user's build machine) is what license_create requires in order to mint an end-user license.json. A first-time user will not have one, and cannot get one until they have run rg_fingerprint — for which they need the package, which they now have.

    So the ordering is: integrate first, request the vendor licence at the end (Section 10.5), mint and run the end-to-end test when it arrives. Everything in Sections 2–9 — CMake, the embedded key, the startup verify, feature gates, asset binding — is written and compiled without it. Say this to the user up front so the gap is expected rather than read as a broken integration, and do not go looking for a vendor licence to "unblock" yourself.

    Never invent, fabricate or self-sign a vendor licence, and never reuse the sample_demo_* credentials from the bundle's demo/ directory for anything but the bundled demo. They are ours, published, and identical for every downloader; a build wired to them verifies against a keypair the whole internet has.


2. Place the package inside the user's repo

If you came through Section 0.1 you have already done this, with the destination confirmed at step 6. Read this section anyway to check the result against the layout below, then move on.

Recommended layout (verify with the user before moving files):

<their-repo>/
  src/                      # their existing source
  CMakeLists.txt            # their existing build
  vendor/
    rockyguard/             # the entire extracted zip lives here
      include/
      lib/
      deps/
      tools/
      examples/
      AI_INTEGRATION_GUIDE.md   # this file
      README.txt

If their repo already has a third_party/ or external/ convention, match it. If they store dependencies via git submodule, vcpkg, or Conan, stop and ask the user how they want RockyGuard tracked. Do not unilaterally invent a new dependency-management scheme.


3. Add the library to CMake

Add this block to the project's top-level CMakeLists.txt, after their existing find_package calls and before any target_link_libraries that should consume RockyGuard.

# --- RockyGuard license library ---
if(NOT DEFINED ROCKYGUARD_DIR)
    set(ROCKYGUARD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/rockyguard")
endif()
if(NOT EXISTS "${ROCKYGUARD_DIR}/include/rockyguard/rockyguard.h")
    message(FATAL_ERROR "RockyGuard not found at ${ROCKYGUARD_DIR}")
endif()

set(_RG_DEPS "${ROCKYGUARD_DIR}/deps")
add_library(rg_openssl_crypto STATIC IMPORTED)
add_library(rg_openssl_ssl    STATIC IMPORTED)
if(WIN32)
    set_target_properties(rg_openssl_crypto PROPERTIES
        IMPORTED_LOCATION "${_RG_DEPS}/lib/libcrypto.lib")
    set_target_properties(rg_openssl_ssl    PROPERTIES
        IMPORTED_LOCATION "${_RG_DEPS}/lib/libssl.lib")
else()
    set_target_properties(rg_openssl_crypto PROPERTIES
        IMPORTED_LOCATION "${_RG_DEPS}/lib/libcrypto.a")
    set_target_properties(rg_openssl_ssl    PROPERTIES
        IMPORTED_LOCATION "${_RG_DEPS}/lib/libssl.a")
endif()
target_include_directories(rg_openssl_crypto INTERFACE "${_RG_DEPS}/include")
target_include_directories(rg_openssl_ssl    INTERFACE "${_RG_DEPS}/include")

add_library(rockyguard STATIC IMPORTED)
if(WIN32)
    # Per-config CRT variants. rockyguard.lib is built with /MD
    # (Release-CRT) and rockyguard_mdd.lib with /MDd (Debug-CRT).
    # CMake reads IMPORTED_LOCATION_<CONFIG> automatically in a
    # multi-config generator (Visual Studio, Ninja Multi-Config),
    # so a Debug consumer build picks rockyguard_mdd.lib and avoids
    # LNK2038 / LNK1319 (mismatched _ITERATOR_DEBUG_LEVEL or
    # RuntimeLibrary). Other configs (RelWithDebInfo, MinSizeRel)
    # map to the Release variant.
    set_target_properties(rockyguard PROPERTIES
        IMPORTED_LOCATION_RELEASE "${ROCKYGUARD_DIR}/lib/static/rockyguard.lib"
        IMPORTED_LOCATION_DEBUG   "${ROCKYGUARD_DIR}/lib/static/rockyguard_mdd.lib"
        IMPORTED_LOCATION         "${ROCKYGUARD_DIR}/lib/static/rockyguard.lib"
        MAP_IMPORTED_CONFIG_MINSIZEREL     Release
        MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release)
else()
    set_target_properties(rockyguard PROPERTIES
        IMPORTED_LOCATION "${ROCKYGUARD_DIR}/lib/static/librockyguard.a")
endif()
target_include_directories(rockyguard INTERFACE "${ROCKYGUARD_DIR}/include")

# Link order matters on Linux: ssl before crypto. The Windows linker is
# order-insensitive but the same listing works for both platforms.
target_link_libraries(rockyguard INTERFACE rg_openssl_ssl rg_openssl_crypto)

if(WIN32)
    target_link_libraries(rockyguard INTERFACE
        ws2_32 crypt32 iphlpapi ole32 oleaut32 wbemuuid advapi32 shell32)
elseif(NOT APPLE)
    target_link_libraries(rockyguard INTERFACE pthread dl)
endif()

# This template links the STATIC library, which is the default and the
# only path with a Debug-CRT variant. If the user insists on the DLL
# (lib/shared/rockyguard.dll, import lib rockyguard.lib, plus
# rockyguard.sig and the two OpenSSL DLLs beside the exe), note that
# the DLL is built /MD ONLY: a Debug (/MDd) consumer links its import
# library without error and then corrupts memory, because std::string /
# vector / map have a different layout under the two CRTs and
# LicenseResult / License cross the boundary by value. Keep the Debug
# configuration on the static rockyguard_mdd.lib, or compile it with
# the Release CRT. Manual 3.1 / 3.2.
#
# You will not get far wrong by accident: since v1.4.0 export.h emits a
# hard #error for a Debug-CRT translation unit that has
# ROCKYGUARD_SHARED_LIB defined, naming both remedies. Do NOT reach for
# the escape hatch (ROCKYGUARD_ALLOW_DEBUG_CRT_WITH_DLL) to make that
# error go away -- it exists only for someone who has built their own
# Debug rockyguard.dll from source, and defining it against the SHIPPED
# DLL restores exactly the silent corruption the guard prevents.

# C++17 is REQUIRED. Putting it on the interface means every target that
# links rockyguard inherits it, so you do not have to remember to set
# CMAKE_CXX_STANDARD in the consuming project -- and a project that pins
# an older standard gets a clear CMake-level conflict rather than a
# compile error deep in a header.
target_compile_features(rockyguard INTERFACE cxx_std_17)
# --- end RockyGuard ---

Do not omit the target_compile_features line. The shipped library is built as C++17 and enforces it with a static_assert in include/rockyguard/export.h, which every public header reaches. MSVC still defaults to C++14, so on Windows a project that has not set the standard itself fails to build with:

error C2338: static_assert failed: 'RockyGuard requires C++17 or later.
In CMake: set(CMAKE_CXX_STANDARD 17); set(CMAKE_CXX_STANDARD_REQUIRED ON).
Direct compiler flag: /std:c++17 (MSVC) or -std=c++17 (gcc/clang).'

Recent g++ and clang default to C++17 or later, so the omission is invisible there — which is exactly why it is easy to ship a CMakeLists.txt that only works on the machine it was written on. If you would rather set it on the consuming project instead of the interface, set(CMAKE_CXX_STANDARD 17) plus set(CMAKE_CXX_STANDARD_REQUIRED ON) is equivalent.

Then, on the target you decided in Section 1.2, add:

target_link_libraries(<their_main_target> PRIVATE rockyguard)

Replace <their_main_target> with the actual add_executable() name in their CMakeLists.

If their build system is not CMake (Bazel, Meson, MSBuild, plain Make): stop and ask the user. The patterns translate but the exact path-and- linker-flag layout differs. Don't guess.


4. Embed the public key

Find the source file containing int main(...). At the top, after the existing #includes, add:

#include <rockyguard/rockyguard.h>

namespace {

// RockyGuard public key. Pairs with the private key on the build machine.
// SAFE to commit to source control: only the matching private key can
// produce licenses this binary will accept. The key below is a placeholder;
// replace it with the contents of public.pem from `tools/license_keygen`.
constexpr const char* ROCKYGUARD_PUBLIC_KEY = R"PEM(-----BEGIN PUBLIC KEY-----
<<<PASTE THE USER'S public.pem CONTENT HERE>>>
-----END PUBLIC KEY-----
)PEM";

}  // namespace

Read the user's public.pem file and substitute its content into the placeholder above. Do not generate, fake, or shorten the key. If you cannot read the file, ask the user to paste it.


5. Add verification at startup

Insert these lines as the first thing main() does, before any substantive work. The pattern:

int main(int argc, char* argv[]) {
    // The constructor is the only verifier call that throws. It raises
    // std::runtime_error if ROCKYGUARD_PUBLIC_KEY is not a parseable
    // PEM public key -- which is exactly what a truncated or mangled
    // paste in Section 4 produces. Uncaught, that is std::terminate():
    // the user sees a hard crash instead of a message. Guard it.
    //
    // std::optional keeps construction inside the try while `verifier`
    // stays in scope, so the existing main() body below does NOT need
    // to be re-indented into a try block. LicenseVerifier is neither
    // copyable nor movable, so emplace() constructs it in place --
    // plain assignment will not compile.
    std::optional<rockyguard::LicenseVerifier> maybe_verifier;
    try {
        maybe_verifier.emplace(ROCKYGUARD_PUBLIC_KEY);
    } catch (const std::runtime_error& e) {
        std::cerr << "License system error: " << e.what() << "\n";
        return 1;
    }
    rockyguard::LicenseVerifier& verifier = *maybe_verifier;

    auto load_result = verifier.load("license.json");
    log_line(load_result.to_json());   // see "Report by code" below
    if (!load_result) {
        std::cerr << "License error: " << load_result.message << "\n";
        return 1;
    }

    // Capture the result. Do NOT write
    // `if (!verifier.check_node_locked())` and throw it away: a result
    // that PASSES can still carry a warning worth telling the user
    // about, and a result that fails carries the code you log and the
    // remedy you show. Nor is a hard-coded "not valid for this machine"
    // line right here -- this call also reports clock manipulation,
    // integrity and expiry failures, so most of what it refuses has
    // nothing to do with the hardware.
    const auto hw_result = verifier.check_node_locked();
    log_line(hw_result.to_json());
    if (!hw_result) {
        std::cerr << "License error: " << hw_result.message << "\n";
        return 1;
    }
    if (rockyguard::error_info(hw_result.code).severity ==
            rockyguard::DiagnosticSeverity::Warning) {
        // Passing, but say something -- expired-but-in-grace, or a
        // clock anomaly that was repaired and will probably recur.
        // Never gate startup on this branch.
        warn_the_user(hw_result.message);
    }

    // ... existing main() body continues here ...
}

Add #include <optional> alongside the other includes if it is not already present, and #include <rockyguard/license_error.h> for error_info() / DiagnosticSeverity (the umbrella <rockyguard/rockyguard.h> already pulls it in, but name it if the project includes the individual headers). Replace log_line() and warn_the_user() with whatever the project actually uses; do not introduce a logging framework it does not already have.

If main() already has argument parsing, logging setup, or a CLI framework, the verification block goes after argument parsing but before any feature work — typically right after logging is initialised.

The "license.json" path is relative to the working directory. Substitute the per-user path chosen in Section 1.3 (resolve %APPDATA% / $XDG_CONFIG_HOME at runtime; do not hard-code a user's home). Whatever the path, its directory must be writable by the running user -- the library writes an anti-rollback witness beside the license and fails silently when it cannot.

Do not catch and swallow license errors. If verification fails, the application must not proceed into licensed functionality. Treating a failed license as success is the most common integration mistake and silently disables protection.

"Must not proceed" is not the same as "must call exit()". How to refuse depends on what you are integrating into, and return 1 from main() is only correct for the first case:

Host type Correct response on failure
Console app / CLI Print the message to stderr and return non-zero from main(). This is the snippet above.
GUI app Show a dialog with result.message -- or, if the dialog has a separate action line, your own headline plus result.suggested_action -- then quit the event loop. Exiting silently before the main window appears is indistinguishable from a crash to the end user.
Service / daemon Log the failure and either stay up in a disabled state or exit deliberately. A bare exit() in a supervised service usually triggers a restart loop that hides the real cause.
Plugin / shared library Return a failure code to the host and disable your features. Never call exit() or abort() here — there is no main() of yours to return from, and terminating takes down the host application (the CAD package, IDE, or DAW that loaded you) along with any unsaved user work.

Ask the user which of these their application is if it is not obvious from the codebase, and pick the matching pattern.

Report by code, never by message text. (v1.4+) Every LicenseResult carries a fine-grained LicenseErrorCode in result.code, alongside the coarse result.status it has always had. The two are orthogonal: each code maps onto exactly one status, and the mapping reproduces what pre-v1.4 releases returned, so status-based code keeps working unchanged. When you generate error handling, follow these rules -- this is the part integrations get wrong in a way that only shows up years later, when a release rewords a sentence and someone's if (msg.find("expired")) quietly stops firing.

  • Branch on result.status for control flow. It is the coarse outcome and it is frozen -- v1.4 added no enumerator to LicenseStatus, precisely so that a customer's exhaustive switch keeps compiling without new -Wswitch warnings.
  • Use result.code when one status covers conditions you want to treat differently. HardwareMismatch covers both HWID_MISMATCH (this really is a different machine) and HWID_MISSING_IN_LICENSE (the licence was minted without a fingerprint -- an issuance bug to route back at the vendor). Expired covers LICENSE_EXPIRED and GRACE_PERIOD_EXPIRED, which are two different conversations to have with a paying customer. ServerUnreachable covers six NETWORK_* transport causes that used to be one opaque "cannot reach license server".
  • Never substring-match, regex, or compare result.message. The code and its SCREAMING_SNAKE_CASE name are documented stable contracts; the wording of message is documented as explicitly NOT one, and it is reworded between releases. If you catch yourself writing a string comparison against message, the code you want already exists -- look it up in Customer_API_Reference §4.5.
  • Log result.to_json(). One line, machine-parseable, carrying code, status, severity, retryable, message, detail, suggested_action and grace days. It is the single thing worth capturing for support and the thing to ask the user to paste into a ticket. Log it on the passing path too -- a clean run's line is what makes a later failure line meaningful. result.to_string() is the shorter form if a JSON line is more than the project's log format wants.
  • Show result.suggested_action as the user-facing "what to do". Do not invent remedy wording; these sentences are written per code and are maintained with the library, so they cannot go stale the way a hard-coded string in the host does. result.message is that same text pre-joined (summary + detail + suggested_action), so print message or the parts -- printing both says the remedy twice.
  • Treat DiagnosticSeverity::Warning as "passing, but say something". error_info(result.code).severity is Warning only on results that convert to true: currently OK_GRACE_PERIOD_ACTIVE and OK_CLOCK_ANCHOR_REPAIRED. Surface it and let the application run. Blocking startup on a licence that passed is a worse bug than not reporting the warning at all.
  • For a floating checkout, retry only on error_info(result.code).retryable, with backoff, and stop as soon as it is false. See Customer_API_Reference §4.6 for the shape.

Two notes on upgrading an existing integration. LicenseStatus is unchanged, so nothing that switches on it needs editing. LicenseResult gained three appended members and therefore changed size, so a project moving to v1.4 must recompile against the new headers -- dropping a v1.4 shared library under a binary built against v1.3 is not supported (RockyGuard_Versioning_Policy section 6).

Full catalog of codes with meanings and remedies: §4.5 of docs/Customer_API_Reference.pdf. The catalog lookups (error_info(), error_code_name(), error_code_from_name(), status_name(), error_catalog(), DiagnosticSeverity) and a worked handling example: §4.6. Read both before writing error handling by hand; do not guess code names, they are listed there.


6. Feature gating (call this out to the user)

This is the part most integrations miss. Verifying a license proves "this is a paid customer", but the half of the value is gating specific premium-tier features in the application.

Before writing any feature checks, ask the user:

"Looking at your product, here are the features I can identify that might be paid-tier: [list 3-7 candidates derived from the codebase — e.g. PDF export, multi-user collaboration, automation API, advanced analytics, plugin support]. Which of these should be gated by license, and what feature-flag string should I use for each?"

Wait for their answer. Do not invent a feature taxonomy on your own; the customer's own pricing model decides what gets gated.

Once they answer, gate each feature with:

if (!verifier.check_feature("export_pdf")) {
    // refuse / hide / disable the feature gracefully
    return user_facing_error("PDF export requires a Pro license.");
}

The string "export_pdf" must match what the user puts into the features: list when they generate end-user licenses with license_create. Record the chosen feature names in a FEATURES.md at the repo root: copy <package>/examples/FEATURES.template.md there and fill it in. It is the one place code, the build (rockyguard_bind_asset), and license minting (license_create --feature <name>, repeated per feature) must agree on every flag string, and it also records which features are bound (§6.5) versus boolean-only (§6.5.7). Keep it current — a flag typo in any of the three fails silently.

check_feature() returns a bool, and a bool is one branch an attacker flips. It is the right tool for greying out a menu or showing "Pro only", but it is not, by itself, enforcement. For each gated feature that loads a mandatory asset, the enforcement is §6.5 — bind that asset to the license. Do §6.5 for those features in the same pass as §6; do not defer it to a later "hardening" phase.

It is also a lookup, not a re-validation: it reads the License cached by load() and never re-checks expiry, the clock or integrity, so a process that stays up for days keeps getting true after the license has expired. The periodic background timer in §9.4 (re-verify with check_node_locked() or check_expiry() and gate on that result) is what closes this for long-running hosts. It is not optional for a service or a desktop application.


6.5 Make each paid feature load-bearing (REQUIRED where a bindable asset exists)

Sections 5 and 6 make licensing work and drive the UI. This section makes it hold. Instead of branching on a verify result — a decision made on the attacker's CPU, so a branch they can flip — you derive a key from the validated license and use it to decrypt data the feature genuinely needs (a lookup table, a resource blob, a model, a ruleset). Patch the check out and the key never appears, so the bytes stay encrypted and the feature produces garbage rather than a working unlock. A bool has two values; a 32-byte key has 2²⁵⁶ — you cannot patch a function into emitting a key it does not possess.

The library ships this as an API with no verdict in it: rockyguard::LicenseAsset (<rockyguard/license_asset.h>) captures the 32-byte key the vendor sealed into the license — the product data key (license_create --wrap-data-key) or one feature's key (--wrap-feature-key <name>=<hex>) — and only when a genuine signature is present, the one field an attacker cannot forge. Its open() returns the decrypted asset or an empty vector; there is no ok(), no operator bool, no std::optional, and no way to ask whether the key arrived. The package ships the build side too: tools/rg_bind_asset seals assets, the rockyguard_bind_asset() CMake helper wires it into the build, and tools/rg_bind_lint catches regressions. (The lower-level LicenseVerifier::unwrap_data_key() / unwrap_feature_key() return std::optional; prefer LicenseAsset, which exists precisely so nobody writes if (!key) around them.)

6.5.1 The rule

For every feature you gated in §6: if the feature loads an asset it cannot function without, bind that asset (steps below) instead of relying on the check_feature() bool. A feature with no bindable asset falls back to §6.5.7 — and you tell the user which features got only bool protection.

6.5.2 Discover bindable assets — run this procedure, then confirm (do not invent)

Do not ask the user an open-ended "what could I encrypt?". Run this and present the result:

  1. For each gated feature, find its entry point and list every file or blob it loads at runtime and cannot run without: resource bundles (.rcc/.qrc/.pak/.dat/.bin), model weights, lookup / rule / pricing tables, shader or parameter blobs, embedded scripting bytecode, schemas.
  2. Score each candidate 0–2 on each axis:
    • Mandatory — the feature is dead or produces garbage without it.
    • Choke-loadable — it enters through one narrow call you can route through a single decrypt.
    • Opaque — wrong bytes are visibly broken, not silently subtly wrong.
    • Small — under a few MB (it is decrypted into memory).
  3. Present a ranked table to the user. Propose binding the top candidate per feature. Ask them only to confirm or correct — not to invent.
  4. Any feature with no candidate scoring ≥1 on both Mandatory and Choke-loadable is boolean-only: handle it per §6.5.7 and say so.

6.5.3 Seal the asset at build time (one tool, one CMake call)

Do not hand-roll AES-GCM. Use the shipped binder; the plaintext never enters the repo or the binary.

  1. Generate the keys (once each) and keep them in the build machine's / CI's secret store, never in the tree:

    <package>/tools/license_create --gen-data-key      # product key        -> RG_DATA_KEY
    <package>/tools/license_create --gen-data-key      # key for export_pdf -> RG_KEY_EXPORT_PDF
    

    One product key for assets every licensee gets, plus one separate key per bound feature for anything tier-gated (§6.5.5 explains why the product key alone gives no tier separation).

  2. Wire the encrypt step into the build with the CMake helper (<package>/cmake/RockyGuardBindAsset.cmake):

    list(APPEND CMAKE_MODULE_PATH "${ROCKYGUARD_DIR}/cmake")
    include(RockyGuardBindAsset)
    
    rockyguard_bind_asset(TARGET  <their_main_target>
        FEATURE  export_pdf                        # must match the feature name in §6
        ASSET    ${CMAKE_SOURCE_DIR}/assets/export_tables.bin
        HEADER   ${CMAKE_BINARY_DIR}/generated/export_tables.enc.h
        SYMBOL   kExportTables
        DATA_KEY_ENV RG_KEY_EXPORT_PDF)            # that feature's key, from the environment
    

    The build reads the key from $RG_KEY_EXPORT_PDF at build time and generates a ciphertext-only header (kExportTables / kExportTables_len). Provide the key to the build environment (CI secret), e.g. RG_KEY_EXPORT_PDF=<hex> cmake --build build. For a product-wide asset point DATA_KEY_ENV at RG_DATA_KEY instead. Under the hood this runs tools/rg_bind_asset (prebuilt in the package, source beside it; the examples CMake also builds it as target rg_bind_asset). The blob it writes is the library's own nonce(12) || ciphertext || tag(16) layout — exactly what LicenseAsset::open() reads; nothing is derived in between.

  3. Wrap the same keys into each end-user license at mint time — the product key into every license, each feature key only into licenses entitled to that feature:

    <package>/tools/license_create ... --feature export_pdf \
        --wrap-data-key <product hex> --wrap-feature-key export_pdf=<feature hex> \
        --output license.json
    

    --feature takes one name and is repeated per feature (--feature export_pdf --feature batch_mode). There is no --features, and a comma-separated value is not split — it becomes one feature whose name contains a comma. license_create refuses to wrap keys into a short evaluation license unless you pass --allow-eval-keys. Do not: a trial carrying production keys hands them to anyone who downloads it.

    If you are testing this integration, do not test it with a trial license. A license expiring within 7 days is an evaluation mint and carries no wrapped key — license_create refuses to put one there, and the licensing portal silently omits it. The integration then behaves as if it were broken: every check passes, but unwrap_data_key() and LicenseAsset::open() return nothing, with no error code to explain it because nothing actually failed. Use a license that expires more than 7 days out for any development or test work on this path.

6.5.4 Open and USE it at runtime — without branching

Nothing to copy but two lines. At the feature's load site — where it used to call check_feature() and branch — construct a LicenseAsset for that feature and feed open()'s result straight into the work:

#include <rockyguard/license_asset.h>
#include "export_tables.enc.h"   // generated in 6.5.3: kExportTables / kExportTables_len
// ...
rockyguard::LicenseAsset asset(verifier, "export_pdf");            // AFTER verifier.load(); cannot fail
load_export_tables(asset.open(kExportTables, kExportTables_len));  // real work; empty/garbage without a genuine, entitled license

For a product-wide asset use rockyguard::LicenseAsset asset(verifier); (no feature name). open() returns the plaintext, or an empty vector when there is no key, the blob is malformed, or authentication fails — and it deliberately does not say which. Do not write if (table.empty()) refuse; around it: that is the branch this section exists to remove. Let the loader fail on nothing. Construct the LicenseAsset after load(); one built earlier captures nothing, silently, by design.

The feature is now incapable of working without the key. There is no verdict to patch — only the plaintext, which never materialises for an unlicensed run. (examples/bound_asset.h is the build-time sealer behind the binder; the application does not need it.)

6.5.5 Per-feature keys are wrapped by the vendor, not derived

The product data key answers "was this license genuinely signed". It does not answer "is this license entitled to this feature": every license carries the product key, so an asset sealed under it opens for every licensee, and entitlement would be back to the check_feature() bool. So for each tier-gated feature you generate a separate key (§6.5.3 step 1) and license_create --wrap-feature-key puts it only into licenses entitled to that feature. A Basic license physically carries no export_pdf key; LicenseAsset(verifier, "export_pdf") captures nothing; a patched check_feature() cannot insert a key the envelope never carried.

Do not "simplify" this into subkeys derived from the product key (HKDF(product_key, feature) or similar). It was evaluated and rejected: every input to that derivation is public or comes from the one product-wide key, so a license of any tier yields every tier's key offline — the appearance of tier separation with none of the substance. The binder seals under whichever key you hand it and derives nothing; the separation comes from what the vendor wraps into each license.

Bind eight features → eight independent decryptions under eight keys, no shared verdict anywhere; patch any one site and only that feature breaks (into garbage), while the others never consulted a check to patch. One leaked plaintext does not compromise other features' assets either, since they never shared a key.

6.5.6 The negative test is MANDATORY (§10 enforces it)

A fake binding passes the normal "it works with a good license" test. Only the negative test distinguishes real binding: with a wrong/foreign license (valid signature, different key), the bound feature must be visibly broken/garbage — not a clean "license invalid" that still lets the rest of the app run. If the feature still works with the wrong key, the binding is fake; do not ship it. See §10.

6.5.7 When a feature has no bindable asset (honest fallback)

Not every feature loads opaque mandatory data. In order:

  1. Manufacture a dependency: find a constant or table the feature's real code already uses (a coefficient set, a state table, embedded defaults), move it into an encrypted asset, and open it with LicenseAsset. Most "pure logic" features have at least one such table.
  2. If there is genuinely nothing to bind, fall back to code-diversity re-checks (§9.4 item 3) and tell the user plainly: "feature X is protected only by a flippable check, because it has no bindable asset." Never leave it looking hardened when it is not.

6.5.8 State the ceiling — do not oversell it

This defeats the free branch-flip / keygen-less crack and forces an attacker to possess a genuine license and reverse your derive/decrypt path — so compile that translation unit under ROCKYGUARD_OBFUSCATE. It does not, for an offline scheme, force signature forgery: one leaked genuine license carries the signature that yields the key (break-once-run-everywhere). It is signature-bound, not machine-bound — keep check_node_locked() (§5) for hardware/expiry enforcement; the key derivation deliberately does not fold in the live fingerprint, so ordinary hardware drift does not break decryption, and it does not fold in time either: offline, the only clock is the end user's, so a key that depended on "not yet expired" would be a branch on the local clock wearing a KDF. Expiry stays a verdict from check_expiry(); treat it as one (§9.4). For leak traceability, use a per-customer data key and record which key went to whom. Full worked example: examples/loadbearing_asset_example.cpp.


7. Floating licensing (Premium tier, only if requested)

Skip this section unless the user has explicitly said:

  • They have a Premium-tier vendor license, AND
  • Their product is multi-seat / multi-user, AND
  • They want concurrent-seat licensing across their customer's LAN.

If yes, see examples/rg_floating_client.cpp in this package. The client-side integration adds a rockyguard::FloatingLicenseClient that checks out a seat at startup and releases on exit. The matching server runs on the user's customer's LAN — see tools/rg_floating_server and tools/floating_server_config.yaml. Walk the user through running the server before adding client-side checkout calls; otherwise the first checkout() returns ServerUnreachable -- as of v1.4 with a code saying which transport failure it was, usually NETWORK_CONNECTION_REFUSED for "nothing is listening" as against NETWORK_HOST_UNRESOLVED for a wrong hostname. Log result.to_json() around a failing checkout and the answer is in the "code" field rather than in a guess.

Heartbeat callbacks run on the client's own thread. If you wire FloatingHeartbeatCallbacks (on_connection_lost, on_connection_restored, on_license_revoked, on_give_up), they fire on the client's internal heartbeat thread. From inside one, never call checkout() or checkin() on that client (rejected with FLOATING_REENTRANT_CALL since v1.4; a crash or deadlock before), and never destroy the client (delete, unique_ptr::reset(), scope exit): the destructor joins the thread the callback is running on, and that self-join terminates the process with no guard possible. Set an atomic flag or post to your UI / work queue, and act from the thread that owns the client. is_checked_out() is safe from a callback. Reference §8.7.2; manual §10.5.4.

Transport security is mandatory-by-default as of v1.3. Both sides fail closed at startup rather than falling back to cleartext, so a configuration that worked against v1.2.x can now refuse to start:

  • Client — FloatingLicenseClient's constructor throws std::runtime_error (before any network call) if use_tls is false and allow_insecure_http is not true, or if use_tls is true with an empty tls_ca_cert_path and allow_insecure_tls not true. Set use_tls = true plus tls_ca_cert_path; use the opt-out flags only for local testing.
  • Server — rg_floating_server exits at startup if neither tls_cert/tls_key nor allow_insecure_http: true is present in the YAML. The shipped floating_server_config.yaml sets allow_insecure_http: true so local evaluation works out of the box; tell the user to replace it with a real cert before deployment.

Note allow_insecure_tls and allow_insecure_http are different axes — the first relaxes certificate verification once TLS is on, the second permits no encryption at all. Setting the first does not enable the second. Both fail closed because the checkout response carries the session secret that HMACs every later request.


8. "Diagnose License" menu item (ask first -- opt-in)

This section is opt-in. Do not add it unless the user asks for it, or you asked and they said yes. Sections 3-6 are the required recipe; this is a support feature on top.

Ask the user:

"Do you want a Help → Diagnose License menu item? It collects a full licensing status report on the end user's machine -- fingerprint, licence verdicts, clock, network, cache state -- and shows it in a dialog, so your support desk can be told what is actually wrong instead of guessing. Optionally it also writes one encrypted file the end user can e-mail you."

If they have no GUI (a CLI tool, a service, a library), say so and skip this section. Do not invent a menu.

8.1 What to add

Two functions, both over <rockyguard/diagnostics.h>:

#include <rockyguard/diagnostics.h>
namespace rgd = rockyguard::diagnostics;

// public_key_pem takes PEM CONTENT, not a path -- your end user has no
// key file and must not need one.
static rgd::Options diagnostic_options() {
    rgd::Options o;
    o.license_path   = "license.json";     // whatever Section 5 uses
    o.public_key_pem = ROCKYGUARD_PUBLIC_KEY;
    o.tool_version   = "<AppName> <version>";
    return o;
}

First, make the key reachable. Section 4 puts ROCKYGUARD_PUBLIC_KEY in an anonymous namespace in the file containing main(), which gives it internal linkage -- it is invisible from any other translation unit. A diagnostics dialog almost always lives in a different .cpp (a UI file), so that constant will not compile there.

Do not paste a second copy of the key. Two copies drift, and the one you did not update becomes a licence that verifies in one code path and fails in the other. Instead move the declaration into a small header both files include:

// rockyguard_key.h  -- new file, next to the file that had main()
#pragma once
inline constexpr const char* ROCKYGUARD_PUBLIC_KEY =
    R"PEM(-----BEGIN PUBLIC KEY-----
... the user's public.pem content, moved verbatim from Section 4 ...
-----END PUBLIC KEY-----
)PEM";

inline constexpr gives it external linkage with no ODR problem, so both translation units share one definition. Delete the anonymous-namespace copy from Section 4's file and #include "rockyguard_key.h" in both.

The menu item -- runs off the UI thread, because collection performs network I/O and can take seconds:

// Kick it off when the menu item is clicked. Returns immediately.
auto job = std::make_shared<rgd::AsyncHealthCheck>(diagnostic_options());

// Then, on a timer / idle callback in your UI loop:
if (job->ready()) {
    const rgd::HealthReport h = job->take();
    show_dialog(h);                       // see 8.2
} else {
    status_label.set_text(job->progress()); // safe from any thread
}

AsyncHealthCheck deliberately calls nothing back. You poll it. That is why there is no callback to marshal into the UI thread -- which is the single easiest thing to get wrong here, and it crashes rather than glitching when you do.

8.2 Rendering the dialog

HealthReport is data, not text. Render it with the user's own widgets:

void show_dialog(const rgd::HealthReport& h) {
    set_title(h.headline);                  // one sentence
    set_colour(h.overall);                  // Info / Warning / Error

    for (const rgd::HealthItem& item : h.items) {
        add_row(icon_for(item.severity),    // your icon set
                item.label,                 // "Licence verification"
                item.value,                 // "HWID_MISMATCH (HardwareMismatch)"
                item.detail);               // what to do about it, may be empty
    }
}

Do not call summarize() for a GUI. It returns fixed-width terminal text; in a proportional font the columns come out ragged. summarize() is for consoles and log files.

8.3 Acting on findings

Branch on item.code. It is the same stable error-code contract as Section 5. The wording of value and detail is not stable.

for (const rgd::HealthItem& item : h.items) {
    if (item.code == "HWID_MISMATCH")       offer_reactivation();
    else if (item.code == "LICENSE_EXPIRED" ||
             item.code == "GRACE_PERIOD_EXPIRED") open_renewal_page();
}

One finding is worth surfacing prominently because nothing else reports it: a "Hardware drift" item with severity Warning. It means the licence still verifies, but a fingerprint component has changed -- so the machine is one hardware change away from failing, with no error anywhere to warn the user. If the user wants a proactive "your licence may stop working soon" prompt, this is the hook.

Do not wire diagnose() into this dialog. There is a root-cause engine in the same header (rgd::diagnose(), API reference 11.8) and it is tempting here, because it turns LICENSE_EXPIRED into "your clock is three days fast, do not buy a renewal". It is written for the SUPPORT DESK, not for the end user's screen: some causes name the vendor's own issuance pipeline, and the anti-rollback remedies double as a tamper recipe for whoever caused the problem. Causes carry a support_only flag for exactly this reason, but nothing filters on it automatically, and the rg_inspect tool RockyGuard ships deliberately does not diagnose either.

Its home is the vendor's side, on the report that comes back: either rg_report_read --diagnose, or diagnose() inside the vendor's own support tooling. If the user asks for root causes in the end-user dialog anyway, that is their call to make -- say what it discloses, filter support_only, and let them decide.

8.4 Offering "send to support" (optional, ask)

If the user wants the end user to be able to mail them the report, ask for an RSA public key and seal h.report_json:

rgd::SealRequest req;
req.recipient_public_key_pem = SUPPORT_RECIPIENT_PUBLIC_KEY;  // RSA, theirs
req.tool_version = "<AppName> <version>";
const rgd::SealOutcome sealed = rgd::seal(h.report_json, req);
if (sealed.ok) write_file("diagnostic.rgdr", sealed.container);

Tell the user plainly:

  • This is a second, separate keypair from their licence-signing key, and it must be RSA: an Ed25519 signing key has no encryption operation and will be rejected. Generate it with tools/license_keygen --algo rsa.
  • The private half stays on their support desk and opens the reports. They read them with rg_report_read (source in examples/inspector/). Never ship rg_report_read or that private key to an end user.
  • If they do not want this, skip it. The dialog alone is useful; often the user reads the verdict aloud on the phone and that ends the ticket.

8.5 Do NOT put this at startup

Health collection is not a startup gate. It costs roughly 100 ms with probe_network = false and several hundred with network on -- worst case far more, because DNS resolution cannot be bounded. It also is not side-effect free: verifying creates the anti-rollback witness files if it finds none.

Keep the Section 5 LicenseVerifier check as the startup path. That is what it is for and it is much cheaper. If the user specifically wants richer context at launch, use health_check() with o.probe_network = false and do it once, off the critical path -- never in a loop, and never on every window open.


9. Hardening the integration against tampering (optional extras)

Sections 3–6 make licensing work; §6.5 is where the enforcement lives and is required wherever a feature has a bindable asset. This section is the optional extras on top: symbol hygiene, anti-substitution for the embedded key, avoiding a central verdict choke point, and folding the integrity check into the key. Do §6.5 first; reach here only if the threat model justifies more.

State the ceiling first, and do not oversell it. Everything here raises attacker cost; none of it is unbreakable. A license verified offline on hardware the attacker controls can always, eventually, be patched out. The one primitive that genuinely resists patching is RockyGuard's own integrity self-check (INTEGRITY_HASH_MISMATCH = patched on-disk binary, INTEGRITY_RUNTIME_MODIFIED = runtime hook / attached debugger), which runs on every check_node_locked(). The techniques below exist to make that self-check hard to locate and bypass, and to remove the cheap wins (a greppable key, a single flippable branch). Tell the user this plainly.

9.1 Measure the binary first — do not guess

The right hardening depends on what is actually exposed, and that differs by target type. Inspect the real artifact before changing anything:

nm -D --defined-only <artifact> | grep -ci rockyguard   # DYNAMIC export table (.dynsym)
nm    --defined-only <artifact> | grep -ci rockyguard   # STATIC symbol table (.symtab)
strings -a <artifact> | grep -c 'BEGIN PUBLIC KEY'      # greppable key blob?
file <artifact> | grep -o 'not stripped\|stripped'      # symbols still present?
  • .dynsym is the export table — what -fvisibility and visibility("default") govern. It matters for a shared library; for an executable it usually holds only imports and a few runtime symbols.
  • .symtab is what plain nm reads. Stripping removes it; visibility flags do not touch it.

9.2 Symbol exposure — the actions differ by target type

Statically-linked executable (the app links librockyguard.a):

  • RockyGuard's functions are internal to the executable, so they are already absent from .dynsym (nm -D returns 0). -fvisibility=hidden therefore changes little on its own here — do not expect it to "hide check_license", because there is no export entry to hide.
  • What leaks the names to nm is .symtab. Stripping is the high-value action — -Wl,-s at link (or strip, or QMAKE/CMake install-strip). It also cannot change the prebuilt .a's baked-in symbol attributes, which is another reason visibility flags do nothing to those symbols.
  • Keep -fvisibility=hidden -fvisibility-inlines-hidden anyway as hygiene: it shrinks the app's own tiny surface and pays off the day the project links the shared RockyGuard lib or builds plugins.
  • Ensure release has no -g (or strip removes it). Keep debuggability with a detached debug file: objcopy --only-keep-debug + --add-gnu-debuglink.

Shared library / plugin (you ship a .so / .dll, or wrap RockyGuard in one):

  • Here the export table is the ABI surface, and -fvisibility=hidden does real work. Default to hidden, then export only the handful the host genuinely calls with __attribute__((visibility("default"))). The license functions (check_*, key accessors, the verifier glue) must not be exported.
  • ELF: for precise control add a linker version script that whitelists exports and makes everything else local:
    # exports.map
    { global: my_plugin_entry; my_plugin_shutdown; local: *; };
    
    -Wl,--version-script=exports.map. Verify with nm -D.
  • Windows DLL: symbols are hidden by default and exported opt-in. The mistake to avoid is the reverse of ELF — do not put __declspec(dllexport) (or a .def entry) on any license symbol. Ship without the .pdb.
  • Strip still applies. Note that if RockyGuard is consumed as its shared librockyguard.so, the vendor already export-controls its symbols; your job is your own wrapper's exports plus shipping librockyguard.sig beside the .so for the integrity check.

Build-system snippets:

  • CMake: set_target_properties(t PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON); strip release via target_link_options(t PRIVATE $<$<CONFIG:Release>:-s>) or install-strip.
  • qmake: in a CONFIG(release, debug|release) block: QMAKE_CXXFLAGS += -fvisibility=hidden -fvisibility-inlines-hidden and QMAKE_LFLAGS += -Wl,-s.

9.3 Do not embed the public key as a greppable blob

Applies to both target types. The threat is substitution, not extraction: a public key is not secret, but a contiguous PEM in .rodata is a clean overwrite target — replace it with the attacker's key and the app verifies licenses they signed.

Use examples/embed_public_key.py to emit the key as two independently XOR-masked byte tables plus a reconstruct-and-cross-check accessor (public_key_pem() returns "" if the tables disagree). Verify against the embedded reconstruction (..._intact()) and fail closed on empty. After this, strings | grep 'BEGIN PUBLIC KEY' returns nothing and a substitution must patch both tables consistently. Rotate the key by regenerating the header; its output is deterministic, so an unchanged key yields an empty diff.

This is masking, not encryption (the mask is derivable from the binary). It becomes a real cost only when the reconstruct/compare TU is compiled under an obfuscator so it cannot be trivially located and NOP'd.

9.4 Kill the central choke point (don't funnel gates through one verdict)

A single startup if (!result) refuse; is a single branch to flip. The instinct is "check in more places" — but the choke point is the verdict, not the number of call sites. Eight gates that all call one predicate — check_node_locked(), or worse a bool is_licensed() wrapper you write around it — are still one patch: flip that function's return once and all eight gates see "valid." Nor does copying the same if into eight inlined sites help: the compiler's common-subexpression elimination and the linker's identical-code-folding (/OPT:ICF, --icf=all) merge identical checks back into one blob, and even when they survive, byte-identical checks are exactly what a pattern-based cracker search-and-replaces in one pass.

So, in order of how much they actually buy you:

  1. Do NOT wrap the check in a single shared boolean predicate of your own. A bool is_licensed() that everything calls adds a customer-side choke point on top of the library's. If you must produce a verdict, produce it at the site, not through a shared function.

  2. Strongest — give each gate its own data dependency, not a shared bool. This is Section 6.5 applied per-gate: each gated feature opens its own required asset through LicenseAsset under its own vendor-wrapped feature key (--wrap-feature-key; §6.5.5 — not subkeys you derive from the product key), woven into that feature's real work. Eight features → eight independent decryptions, no shared verdict anywhere. Patch any one site and only that feature breaks — into garbage, not a bypass — and the others never consulted a verdict to patch. Crucially, routing many gates through the single unwrap_data_key() function is fine even though it is one function: you cannot patch a function to emit a 32-byte key you do not possess (a bool has two values; a key has 2²⁵⁶). That is the whole difference between returning a verdict and returning key material.

  3. For paths that genuinely must yield a verdict (startup, greying out a menu item), still verify at several independent sites — each calling check_node_locked() so each re-runs the integrity self-check — and make the sites do distinct real work so CSE/ICF cannot fold them together (a volatile sprinkle is not enough). Treat this code-diversity layer as secondary to (2): it raises the cost against scripted/pattern crackers but a determined reverser defeats it, and it cannot touch the library's own single verdict function — that is what ROCKYGUARD_OBFUSCATE and the integrity self-check defend.

    • Feature entry points — re-verify before performing a gated action, throttled (e.g. once per 30 s) so per-click cost is bounded.
    • A periodic background timer — re-verify every few minutes so a session whose startup check was patched out does not run indefinitely.

Which real computations can carry the dependency in (2) is app-specific — ask the user. Do not invent it: ask which of the gated features consume a lookup table, resource blob, model, ruleset, or other data you can encrypt under the license key, and wire the dependency into those. Where a feature has no natural data to bind, fall back to (3) for that one.

Choose the failure response by host type (this mirrors the Section 5 table):

  • Executable: feature-site failure should refuse the action (do not exit() mid-click — you would destroy unsaved work); the periodic timer is the site that may tear the session down.
  • Plugin / shared library: never exit()/abort() at any site — that kills the host. Return failure to the host and disable your features.

9.5 Strongest: make the license load-bearing (consume the result, don't branch on it)

MOVED — this is now §6.5 and is part of the required recipe. As of this guide revision the load-bearing pattern ships as a tool (tools/rg_bind_asset), a CMake helper (rockyguard_bind_asset()), the LicenseAsset class (<rockyguard/license_asset.h>), a worked example (examples/loadbearing_asset_example.cpp), and a linter (tools/rg_bind_lint). Do it from §6.5, which supersedes the prose below. The text here is retained as the conceptual explanation and is referenced by §9.6.

Everything above raises the cost of patching the license decision. This step removes the decision as the thing worth patching. Instead of branching on the verify result — a choice made on the attacker's CPU, so a branch they can flip — derive a key from the validated license and use it to decrypt data or code the program genuinely needs (a required lookup table, a resource blob, a critical routine). Patch the check out and the key never appears, so the bytes stay encrypted and the app produces garbage rather than a working unlock. A branch can be flipped; a missing decryption key cannot be wished away.

As of v1.4 the library supports this directly, so you do not hand-roll the key derivation:

  1. Make one data key per product and keep it on your build machine:
    <package>/tools/license_create --gen-data-key      # 64 hex chars = 32 bytes
    
  2. Encrypt the asset your program needs, under that key, at build time. examples/loadbearing_asset_example.cpp --encrypt <dk_hex> <in> <out.enc> does it with AES-256-GCM in the exact format the runtime expects. Embed the ciphertext in your binary (a byte array in a header); the plaintext never ships.
  3. Wrap the key into every end-user license at mint time:
    <package>/tools/license_create ... --wrap-data-key <dk_hex> --output license.json
    
    This seals the key into the license, bound to that license's signature.
  4. At runtime, recover the key and decrypt — without branching on it:
    // Do NOT write `if (!key) refuse;` -- that reintroduces the flippable
    // branch. Feed the key straight into your decryption and USE the result;
    // if the license is absent/foreign/tampered, unwrap_data_key() returns
    // nullopt, the decrypt fails, and the program has no usable data.
    std::vector<uint8_t> key;
    if (auto k = verifier.unwrap_data_key()) key = *k;   // empty on any failure
    auto table = my_aes_gcm_open(key, embedded_asset_blob); // your decrypt
    use_the_table(table);                                   // real work; garbage if empty
    

unwrap_data_key() returns the exact 32 bytes you passed to --wrap-data-key, and only when a genuine signature is present: the wrap key is HKDF-SHA256(license signature), the one field an attacker cannot forge for a payload of their choosing. A tampered signature, a corrupted blob, a foreign license, or an absent one all yield nullopt (GCM authentication failure), never a forged key.

State the ceiling to the user, do not oversell it. This defeats the free branch-flip / keygen-less crack and forces an attacker to possess a genuine license and reverse your derive/decrypt path — so compile that translation unit under ROCKYGUARD_OBFUSCATE. It does not, for an offline scheme, force signature forgery: one leaked genuine license carries the signature that yields the key. It is signature-bound, not machine-bound — keep check_node_locked() for hardware binding; the key derivation deliberately does not fold in the live fingerprint, because that would break decryption on the ordinary hardware drift the 2-of-4 match threshold is designed to tolerate. Pick a small, mandatory asset so wrong-key decryption is visibly broken, not a quietly disabled feature.

Full worked example end to end: examples/loadbearing_asset_example.cpp.

9.6 Folding a code hash into the key — evaluated, and NOT recommended

An earlier revision of this guide recommended, as an advanced step, folding a hash of the binary's own code into the key derivation — K = KDF(DK, hash(R)), so a patched .text derives the wrong key and the asset decrypts to garbage with no integrity branch to NOP. We have since red-teamed it against the same attacker model as the rest of this section, and we now advise against it. It does not reliably raise the bar, and it can lower it. This subsection keeps the reasoning on record in place of the construction.

Why it looked good. An isolated integrity check (if (hash != expected) refuse;) is just another patchable branch. Feeding the hash into the key instead of into a branch removes that branch — the same "no verdict to flip" property that makes 9.5 load-bearing.

Why it is not load-bearing. 9.5 works because the value it folds in — the signature — is a secret: an attacker cannot produce the key without possessing a genuinely signed license. hash(R) has no such property. The expected code hash is not secret — it is a deterministic function of the legitimate binary the attacker already holds. So the attacker never has to touch the crypto. They keep a pristine copy of R (or the known-good constant) and make the running image hash that instead of itself: a one-line redirect of the pointer or length handed to the hasher.

And this is what makes it worse than doing nothing: the self-hash routine is a more distinctive, more greppable target than the branch it replaced. A lone if (status != Valid) is one of thousands of comparisons; a routine that reads its own code section and feeds the result into a KDF is a recognizable shape with recognizable call sites. Adding it can hand the reverser an easier landmark than the jne you were trying to eliminate. A mechanism whose only net effect is to move the patch target somewhere more obvious has negative value.

What to do instead — the steps that fold in a real secret or genuinely raise cost:

  • 9.5 (signature-bound key) is the load-bearing step. Keep it: the signature is the secret the attacker must possess, and no code patch substitutes for it.
  • ROCKYGUARD_OBFUSCATE raises the cost of locating any target in the library's own code, a would-be self-hash included. It is friction, not a secret — the right tool for "make the code hard to navigate," and the wrong one to mistake for a lock.
  • The shipped .sig and the runtime integrity check are the detection layer: they tell an honestly-deployed process that its binary was altered. They are not a substitute for the load-bearing key, and — being trust-on-first-use in a static build — they do not catch a binary that was patched on disk before first run. Treat them as detection and hygiene, not as the thing that stops this attack.

The permanent ceiling is the one stated in 9.5: an offline check on hardware the attacker controls raises cost, it does not prevent. Do not present a code-hash-in-the-key layer, to yourself or to a reviewer, as the step that closes that gap — it does not.

9.7 Worked example (static Qt executable)

For a static-linked Qt/qmake app the effective set was: strip release (-Wl,-s) + -fvisibility=hidden (hygiene); masked key via embed_public_key.py with a fail-closed intact check at startup; and three check sites — startup, throttled feature-gate re-checks, and a 5-minute periodic re-verify that quits on hard failure. Result, measured: .dynsym rockyguard symbols 0 (already), .symtab 454 → 0 after strip, grep 'BEGIN PUBLIC KEY' 1 → 0, tamper of one masked byte → key fails to reconstruct → app refuses to start.


10. Verification

After making the changes above:

  1. Build the project with the user's normal build command. The build should succeed with no new warnings or errors. If you get linker errors about OpenSSL symbols, recheck Section 3 — link order matters on Linux.

  2. Generate a test license:

    <package>/tools/license_create \
        --key <user's private.pem> \
        --vendor-license <user's vendor_license.json> \
        --id TEST-001 --licensee "Dev Test" \
        --product "<their product>" \
        --type node_locked --threshold 2 --fingerprint \
        --expires permanent --output license.json
    

    (--threshold 2 = match 2 of 4 hardware components, the recommended default for node-locked. --threshold 0 = ignore hardware, useful only for in-CI testing, never ship.)

    --fingerprint is REQUIRED here and you must not drop it. It binds the test licence to the machine you are running on, which is what makes step 3 pass. A node_locked licence with --threshold 2 and NO fingerprint flag is rejected at verification time with status HardwareMismatch and code HWID_MISSING_IN_LICENSE -- deliberately, as a safety net against issuance bugs (Customer_API_Reference §6.4 and §4.5). Match on the code, not on the sentence: v1.4 reworded this one, so a check written against the old text ("Node-locked license has no hardware fingerprint") no longer fires. Note that license_create still exits 0 and prints a success line when you omit it, so the mint gives you no warning; the failure appears only at step 3, and it looks exactly like a broken integration. Earlier revisions of this guide omitted the flag and told you to expect success, which could not happen. If you are minting for a real end user, use --fingerprint-value "<their hash>" instead; --fingerprint means THIS machine and must never ship.

  3. Run the binary with license.json in the working directory. Expected: program runs normally. Then rename license.json and run again. Expected: program prints the license error and exits 1 (with the logging from Section 5 in place, the failing run also emits one to_json() line whose "code" is LICENSE_FILE_UNREADABLE -- that line is the fastest way to confirm the error path is wired up). If both behaviours match, the integration is verified. If instead the first run fails with code HWID_MISSING_IN_LICENSE, the licence in step 2 was minted without --fingerprint. Re-mint it; nothing is wrong with the integration.

  4. Print the license info on first run so the user can sanity-check:

    // license() returns a snapshot by value, copied under the verifier's
    // internal lock, so it stays valid even if another thread reloads.
    const rockyguard::License lic = verifier.license();
    std::cerr << "Licensed to: " << lic.licensee
              << " (license ID " << lic.license_id << ")\n";
    

    Wrap in #ifdef DEBUG if the user prefers.

  5. The negative test — MANDATORY for every feature you bound in §6.5. A binding that only passes the happy path may be fake. Mint a second license from different keys (or reuse a foreign license), run the app with it, and exercise each bound feature. Expected: the feature is visibly broken / produces garbage / is inert — NOT a clean "license invalid" while the rest of the app runs. If a bound feature still works under the wrong key, the plaintext is reachable without the key: the binding is fake and must be fixed before shipping. The shipped example demonstrates both runs:

    # genuine license -> asset opens, feature works
    loadbearing_asset_example --pubkey public.pem --license good.json \
        --feature export_pdf --asset export_tables.enc
    # foreign/absent license -> feature INERT (exit 3), asset never decrypts
    loadbearing_asset_example --pubkey public.pem --license foreign.json \
        --feature export_pdf --asset export_tables.enc
    
  6. Run the binding linter and treat ERRORs as blocking:

    python <package>/tools/rg_bind_lint <their-source-dir>
    

    It catches the ways this pattern silently regresses to a flippable boolean: branching on unwrap_data_key() / unwrap_feature_key(), an if (asset.empty()) refuse; guard, a shared bool is_licensed() predicate, a key that is recovered but never opens an asset, message-text branching, and a missing negative test. "Done" is: the build is clean, step 3 passes, step 5's negative test shows a broken feature, and rg_bind_lint reports zero errors — not merely "it compiles."


10.5 Hardware fingerprint, and the 30-day Premium trial

Run this once the integration builds. It is the last step of the recipe, and for a first-time user it is the step that makes the integration testable at all: without a vendor licence they cannot mint a license.json, and without a fingerprint we cannot issue one.

10.5.1 Read the fingerprint

# Linux / macOS
FP=$(<package>/tools/rg_fingerprint)
echo "$FP"

# Windows PowerShell
$FP = & <package>\tools\rg_fingerprint.exe
$FP

The fingerprint — and only the fingerprint — goes to stdout, as a single line. The banner, any error text and the -v component dump go to stderr, precisely so $(...) and > capture the bare value. Do not scrape it out of a combined transcript; you will capture the banner too.

Add -v if the user wants to see which hardware components feed the hash. Do not pass -v on the run you capture from — capture and display are separate concerns, and mixing them is how a banner ends up pasted into a licence request.

10.5.2 Show it to the user and explain what it is

Print the value and say, in substance:

  • It is a hash of stable hardware identifiers of this machine — their build machine. Not their customers' machines.
  • It binds the vendor licence, which authorises them to mint end-user licences. It is not the fingerprint that ends up in any licence they ship.
  • Rocky Software needs it to issue the licence, and it will travel with the request.
  • If they later change build machines, the licence needs reissuing. That is normal; they email and ask.

Do not describe it as anonymous. It identifies a machine — that is its entire job — and a user deciding whether to send it is entitled to an accurate description rather than a reassuring one.

10.5.3 Ask — then do exactly what they said

Ask one direct question: may you send a 30-day Premium trial request to Rocky Software on their behalf? Then follow the branch. If they ask what the trial covers, the terms are in Section 13.

If the user says no, or does not answer clearly: print the fingerprint and the URL https://rockyguard.dev/contact?intent=trial and stop. Say the form pre-selects the right reason and has a field for the fingerprint. This is a perfectly good outcome — say so, rather than re-asking.

If the user does not want you to send it but wants the form filled: same URL, and hand them the exact values to paste. Do not navigate a browser to submit it for them; that is the same action wearing different clothes.

If the user says yes: collect name, email and company (company optional) — ask, do not guess from git config or the environment, and do not reuse an address you happened to see in the repository. Then submit:

curl -fsS -X POST https://rockyguard.dev/api/contact \
  --data-urlencode "reason=trial" \
  --data-urlencode "name=<their name>" \
  --data-urlencode "company=<their company, or omit>" \
  --data-urlencode "email=<their email>" \
  --data-urlencode "fingerprint=$FP" \
  --data-urlencode "message=<one line: what they are integrating into>"

Notes on that call, so you can read a failure instead of retrying blindly:

  • reason=trial is the slug for the 30-day Premium trial. The other valid slugs are listed at https://rockyguard.dev/contact; use trial unless the user asked for something else by name. (free-dev, the slug of the retired free Developer tier, is still accepted as an alias for trial; do not send it.)
  • Send no website field. It is a honeypot: any value at all makes the endpoint silently discard the submission and still answer 200.
  • name, email and message are required. 400 name length / 400 message length mean an empty field, not a long one.
  • The endpoint checks that the email domain resolves in DNS. 400 email domain does not exist means a typo in the address — show the user, do not "fix" it yourself.
  • 503 means the contact endpoint is misconfigured on our side. Fall back to the form URL and tell the user it is our fault, not theirs.

On success, tell the user the request went to Rocky Software and that the vendor licence arrives by email. Do not promise a turnaround time.

10.5.4 What happens when the licence arrives

Leave the user with the three steps they will need, so the gap between today and the email is a wait rather than a second integration session:

  1. Mint a test licence with the license_create invocation in Section 10, step 2 — copy it verbatim, including --fingerprint. Dropping that one flag produces a licence that mints successfully, exits 0, and then fails verification with HWID_MISSING_IN_LICENSE, which reads exactly like a broken integration. Section 10 explains why.
  2. Check it outside the application, so a failure at step 3 is unambiguously the integration and not the licence:
    <package>/tools/license_verify \
        --key public.pem --license license.json --check-hardware
    
  3. Run the application against it, then run the rest of Section 10 — including the negative test in step 5 and rg_bind_lint in step 6. The integration is not finished until those pass.

One trap worth stating in the same breath, because it presents as a broken integration: do not test with a short-dated licence. A licence expiring within 7 days is treated as an evaluation mint and carries no wrapped key — license_create refuses to put one there. Every check still passes, but unwrap_data_key() and LicenseAsset::open() return nothing, with no error code to explain it, because nothing failed. Use an expiry more than 7 days out for any development or test licence. (The same rule is the reason short-dated licences do not spend the trial's 50-licence budget — see Section 13.)


11. Hard-no rules

You MUST NOT do any of the following:

  • Do not download the package without asking first (Section 0.1, step 3). State the product and version, the URL, the size in bytes, the SHA-256 you will verify against, and where on disk it is going — then wait for an explicit yes. Fetching and running binaries from the network is the user's call, not yours.
  • Do not continue past a SHA-256 mismatch (Section 0.1, step 5). Delete the file, say the digest did not match, stop. Not "retry", not "extract it to check", not "proceed with a note in the summary."
  • Do not hard-code a download URL. Resolve it from https://rockyguard.dev/downloads/latest.json. The zip filenames carry the version and rotate on every release.
  • Do not send a licence request without an explicit yes (Section 10.5.3). The hardware fingerprint identifies the user's machine and the submission is made in their name and from their email address. No pre-filling-and-submitting, no "I'll send it and you can retract it", and no driving a browser to the form on their behalf. If they have not said yes in this conversation, print the fingerprint and the URL and leave it with them.
  • Do not invent, fabricate or self-sign a vendor licence, and do not wire the build to the sample_demo_* keys from the bundle's demo/ directory. Those are ours, published, and identical for every downloader. A first-time user simply has no vendor licence yet — that is expected, and Section 10.5 is how it gets resolved.
  • Do not commit the user's private.pem to git. Add it to .gitignore if it lives anywhere inside the repo.
  • Do not ship private.pem in the application binary, package, or installer. It only ever exists on the user's build machine.
  • Do not catch and ignore license verification errors. The program must exit on failure.
  • Do not branch on result.message text. No find(), no regex, no equality test against a message string, and no parsing it apart. Branch on result.status and result.code, which are stable contracts; the wording is not one and does change between releases (Section 5, and Customer_API_Reference §4.5).
  • Do not embed license.json in the binary. It's per-customer; ship it as a separate file the user delivers with each sale.
  • Do not generate or fake a public key. Always read the user's actual public.pem content; ask if you can't find it.
  • Do not skip Section 6 (feature gating). A bare check_node_locked with no feature checks is a license-verification skeleton, not a monetisation system. The user is paying for the feature-gating layer.
  • Do not silently switch the user's build system (e.g. CMake → Bazel) to make integration easier. Match what they have.
  • Do not call health_check() or collect() on a UI thread. Use AsyncHealthCheck (Section 8.1). Collection does network I/O and can take seconds; blocking the UI thread freezes the application.
  • Do not use the diagnostic collection as the startup licence check. Section 5 is the startup path. Section 8.5 explains why.
  • Do not ship rg_report_read, or the report private key, to an end user. That binary and that key belong on the user's support desk. rg_inspect / the in-app dialog is what goes out.
  • Do not reuse the licence-signing key as the report recipient key. It is Ed25519 in the default setup and cannot encrypt at all. Mint a separate RSA pair (Section 8.4).
  • Do not branch on HealthItem::value or ::detail. Same rule as result.message: branch on HealthItem::code.
  • Do not deliver a paid feature whose only enforcement is a branch on a verdict when that feature loads a bindable asset. Bind it (§6.5). If it has no bindable asset, say so explicitly (§6.5.7) — do not leave it looking hardened.
  • Do not write if (!unwrap_data_key()) refuse; (or if (key.empty()) refuse;, or if (table.empty()) refuse; after LicenseAsset::open()). Each reintroduces the exact flippable branch §6.5 exists to remove. Use LicenseAsset, which has no optional to test, and consume open()'s result.
  • Do not hand-roll the asset encryption. Seal with tools/rg_bind_asset / rockyguard_bind_asset() and open with LicenseAsset; a bespoke AES-GCM with the wrong nonce handling or a leaked plaintext buffer is worse than no binding.
  • Do not derive per-feature keys from the product key. Generate a separate key per bound feature and wrap it with --wrap-feature-key (§6.5.5). HKDF(product_key, feature) gives every licensee every tier.
  • Do not commit the plaintext asset or the data key. The binder reads the key from the environment and emits ciphertext only; keep the plaintext and the --gen-data-key value out of the repo (same rule as private.pem).
  • Do not declare the integration done on a clean build alone. It is done only when the §10 negative test shows a broken feature under a wrong license and rg_bind_lint reports zero errors.

12. When to stop and ask the user

Stop and ask, do not guess, when:

  • https://rockyguard.dev/downloads/latest.json is unreachable, or does not list the platform slug you need (Section 0.1, step 2). Do not guess a filename and do not scrape the download page for a link.
  • The host is arm64 Linux (unsupported) or Apple Silicon (the x86_64 bundle runs under Rosetta, but their application must then be built x86_64 too). Section 0.1, step 1.
  • The user's glibc is older than 2.34 on Linux.
  • You would need to clear the macOS quarantine attribute — say what the command does and why, and let the user decide (Section 0.1, step 6).
  • You can't unambiguously locate int main(...).
  • The project has multiple binaries and it's unclear which to protect.
  • Their build system is not vanilla CMake.
  • They already have a different license-checking system in place (Sentinel, FlexLM, custom). Ask whether to replace it or run side-by-side.
  • Their public.pem is not in the workspace and they haven't pasted its content.
  • They haven't told you which features should be gated.
  • You're about to modify a file you don't recognise.
  • The user asked for a "Diagnose License" menu (Section 8) but the application has no GUI, or you cannot tell where its menus are built.
  • The user wants the "send to support" flow (Section 8.4) but has not provided an RSA recipient public key. Do not substitute their licence key; it cannot encrypt.

13. References

  • API surface: docs/Customer_API_Reference.pdf (every public class, struct, enum, and CLI flag; §4.4-4.6 are LicenseResult, the LicenseErrorCode catalog with a remedy per code, and the <rockyguard/license_error.h> lookups)

  • Full integration manual: docs/Customer_Documentation.pdf (deeper coverage of fingerprinting, anti-tampering, floating server, troubleshooting)

  • Working example: examples/node_locked_example.cpp (this is what Section 5's pattern is distilled from)

  • Floating example: examples/rg_floating_client.cpp

  • Health-check example: examples/health_check_example.cpp (Section 8; shows both the async menu pattern and the cheap startup variant)

  • Hardening (Section 9):

    • examples/embed_public_key.py (the masked-key generator, Section 9.3), plus examples/hidden_key_example.cpp and its generated examples/hidden_public_key.h (node-locked verification with a masked, cross-checked embedded key — the Section 9.3 anti-substitution pattern end to end). Regenerate hidden_public_key.h from your own key; the shipped one is a placeholder.
  • Load-bearing binding (Section 6.5, required where a feature has a bindable asset) — this is a different threat from 9.3: 9.3 stops the embedded key being substituted; 6.5 makes a patched-out check yield garbage. The pieces:

    • <rockyguard/license_asset.h> — LicenseAsset, the runtime side: the class with no verdict in it (Customer_API_Reference §12).
    • examples/loadbearing_asset_example.cpp — end to end: --encrypt at build time and verify → LicenseAsset → open → use at runtime, including the negative (wrong-license) behaviour (exit 3).
    • tools/rg_bind_asset (source tools/rg_bind_asset.cpp) — build-time binder: seals an asset under a key into the library's blob layout and emits a ciphertext header.
    • examples/bound_asset.h — the header-only sealer behind the binder and the example's --encrypt. Build-time only; the application does not need it.
    • cmake/RockyGuardBindAsset.cmake — rockyguard_bind_asset(), wires the encrypt step into the build (plaintext never enters the tree).
    • tools/rg_bind_lint — catches regressions to a flippable boolean; run it in Section 10 and treat ERRORs as blocking.
    • Mint side: license_create --gen-data-key, --wrap-data-key (product key, every license) and --wrap-feature-key <name>=<hex> (per feature, entitled licenses only).
    • examples/FEATURES.template.md — copy to the repo root as FEATURES.md; the shared register of flag strings, per-feature enforcement (bound vs boolean-only), bindings, and the mint command (Section 6).
  • Diagnostics API: docs/Customer_API_Reference.pdf §11 -- §11.6 is the structured HealthReport, §11.7 is AsyncHealthCheck, §11.8 is diagnose() (support-desk side only -- see the warning in Section 8.3)

  • Root-cause catalog: docs/Customer_Documentation.pdf §10.1.2, the list of cause ids and what each asserts

  • Building the standalone Inspector tools: examples/inspector/README.txt (only if the user wants a separate executable to hand out rather than an in-app dialog)

  • Download manifest (Section 0.1): https://rockyguard.dev/downloads/latest.json — the stable, machine- readable index of the current release: per platform, the zip URL, its SHA-256, its size in bytes, and the directory it extracts to. Resolve every download from here. Digests are also published as https://rockyguard.dev/downloads/SHA256SUMS.txt.

  • Agent entry point: https://rockyguard.dev/llms.txt — the short index an agent lands on when given only the domain. Names this guide, the manifest, and the two consent rules above.

  • This guide, canonical: https://rockyguard.dev/AI_INTEGRATION_GUIDE.md (raw markdown) and https://rockyguard.dev/docs/ai-integration (rendered, for humans). Refreshed every release; prefer it over a copy you were handed.

  • Bundle contents: the download is the full SDK, not a toy. It carries include/, both lib/static/ and lib/shared/, deps/, docs/, cmake/RockyGuardBindAsset.cmake, and a tools/ directory with license_keygen, license_create, license_verify, rg_fingerprint, rg_bind_asset, rg_bind_lint and rg_inspect — everything Sections 2 through 10 call for. It also adds a demo/ directory holding a self-contained 30-line program that exercises Section 5; that directory is the only thing in the bundle the word "demo" describes. The floating-licence tooling (Section 7) and the admin portal are the only parts held back for the full customer zip, which Rocky Software sends along with a trial or paid vendor licence.

    (Through v1.4.0 the artifact itself was named ...-demo.zip. It is now ...-sdk.zip. If you are holding an older link or an older copy of this guide, re-resolve it from the manifest rather than reconstructing the filename.)

  • 30-day Premium trial (Section 10.5): https://rockyguard.dev/contact?intent=trial — the request form, with the reason pre-selected and a field for the rg_fingerprint output. No credit card. The vendor licence arrives by email, bound to the build machine's fingerprint, together with the full customer zip. The trial is full Premium, floating licensing included, on one build machine, good for up to 50 end-user licences that expire more than 7 days out; licences expiring within 7 days (already-expired test licences included) are metered separately and do not count against the 50. It is for evaluation and pilots and does not grant the right to sell licences to paying end users — that needs Basic or Premium. Extension on request. When the user buys, the vendor licence is re-issued against the same fingerprint. Terms: https://rockyguard.dev/pricing.

  • Basic or Premium (paid): https://rockyguard.dev/contact — direct the user here to buy, or to ask about anything the trial does not cover.